330 lines
12 KiB
Python
330 lines
12 KiB
Python
import displayio
|
|
import terminalio
|
|
|
|
from time import monotonic_ns, sleep
|
|
|
|
from adafruit_display_text import label
|
|
from macropad_os import AppState, InvalidStateUpdateError, Config
|
|
from macropad_os.app_utils import Macro
|
|
|
|
DISPLAY = displayio.Group()
|
|
|
|
|
|
def convert_to_keynum(x, y):
|
|
return 3 * x + y
|
|
|
|
|
|
# TODO: Limit sounds to a similar rate as well for better performance
|
|
|
|
MAX_LIGHTING_UPDATE_RATE = 33000000 # .033 seconds
|
|
|
|
|
|
class App(object):
|
|
def __init__(self, macropad, config: Config):
|
|
"""
|
|
|
|
:param macropad:
|
|
:param config:
|
|
"""
|
|
print("app created")
|
|
self._key_lights = [(0, 0, 0) for _ in range(12)]
|
|
self._display_group = DISPLAY
|
|
self._title = "title"
|
|
self._layout = None
|
|
self._title_label = label.Label(
|
|
y=4,
|
|
font=terminalio.FONT,
|
|
color=0x0,
|
|
text=f"",
|
|
background_color=0xFFFFFF,
|
|
)
|
|
self._pressed_keys = []
|
|
self._key_pressed_callbacks = []
|
|
self._key_released_callbacks = []
|
|
self._encoder_changed_callbacks = []
|
|
self._encoder_state = 0
|
|
self._labels = []
|
|
self._state = AppState.STOPPED
|
|
self._name = "app"
|
|
self._key_tones = {}
|
|
self._last_lighting_update = 0
|
|
|
|
self._current_brightness = config.brightness()
|
|
|
|
self.macropad = macropad
|
|
self.keyboard = macropad.keyboard
|
|
self.config = config
|
|
|
|
def start(self) -> None:
|
|
print("Start from base class ")
|
|
if self._state is not AppState.STOPPED:
|
|
raise InvalidStateUpdateError(f"Start called but the current app state is {self._state}")
|
|
self._state = AppState.STARTING
|
|
self._on_start()
|
|
self.on_start()
|
|
self._state = AppState.PAUSED
|
|
|
|
def _on_start(self) -> None:
|
|
pass
|
|
|
|
def on_start(self) -> None:
|
|
raise NotImplementedError("on_start not implemented")
|
|
|
|
def resume(self) -> None:
|
|
if self._state is not AppState.PAUSED:
|
|
raise InvalidStateUpdateError(f"Resume called but the current app state is {self._state}")
|
|
self._state = AppState.RESUMING
|
|
self._on_resume()
|
|
self.on_resume()
|
|
self._state = AppState.RUNNING
|
|
|
|
def _on_resume(self) -> None:
|
|
self.add_displays_to_group()
|
|
|
|
def on_resume(self) -> None:
|
|
raise NotImplementedError("on_resume not implemented")
|
|
|
|
def pause(self) -> None:
|
|
if self._state is not AppState.RUNNING:
|
|
raise InvalidStateUpdateError(f"Pause called but the current app state is {self._state}")
|
|
self._state = AppState.PAUSING
|
|
self._on_pause()
|
|
self.on_pause()
|
|
self._state = AppState.PAUSED
|
|
|
|
def _on_pause(self) -> None:
|
|
self.keyboard.release_all()
|
|
self.macropad.consumer_control.release()
|
|
self.macropad.mouse.release_all()
|
|
self.macropad.stop_tone()
|
|
self.macropad.pixels.show()
|
|
self.remove_displays_from_group()
|
|
|
|
def on_pause(self) -> None:
|
|
raise NotImplementedError("on_pause not implemented")
|
|
|
|
def loop(self) -> None:
|
|
# We'll fire you if you override this method.
|
|
self._on_loop()
|
|
self.on_loop()
|
|
|
|
def on_loop(self) -> None:
|
|
raise NotImplementedError("Not implemented")
|
|
|
|
def _on_loop(self) -> None:
|
|
self._update_lighting()
|
|
self._process_keys_pressed()
|
|
self._process_wheel_changes()
|
|
self.on_loop()
|
|
|
|
def _process_keys_pressed(self) -> None:
|
|
key_event = self.macropad.keys.events.get()
|
|
if key_event:
|
|
if key_event.key_number < 12:
|
|
if key_event.pressed:
|
|
self.macropad.stop_tone()
|
|
self._play_tone_for_key(key_event.key_number)
|
|
if self._key_pressed_callbacks:
|
|
for callback in self._key_pressed_callbacks:
|
|
callback(key_event.key_number)
|
|
else:
|
|
self._stop_tone_for_key(key_event.key_number)
|
|
if self._key_released_callbacks:
|
|
for callback in self._key_released_callbacks:
|
|
callback(key_event.key_number)
|
|
|
|
def _play_tone_for_key(self, key_number):
|
|
if self.config.key_tone_enabled():
|
|
if key_number in self._key_tones:
|
|
self.macropad.start_tone(self._key_tones[key_number])
|
|
else:
|
|
self.macropad.start_tone(self.config.key_tone_hz())
|
|
self._pressed_keys.append(key_number)
|
|
|
|
def _stop_tone_for_key(self, key_number):
|
|
self.macropad.stop_tone()
|
|
if key_number in self._pressed_keys:
|
|
self._pressed_keys.remove(key_number)
|
|
if self._pressed_keys and self.config.key_tone_enabled():
|
|
if key_number in self._key_tones:
|
|
self.macropad.start_tone(self._key_tones[self._pressed_keys[0]])
|
|
else:
|
|
self.macropad.start_tone(self.config.key_tone_hz())
|
|
|
|
def _process_wheel_changes(self) -> None:
|
|
encoder = self.macropad.encoder
|
|
if self._encoder_state != encoder:
|
|
for callback in self._encoder_changed_callbacks:
|
|
if self._encoder_state > encoder:
|
|
callback(-1)
|
|
else:
|
|
callback(1)
|
|
self._encoder_state = encoder
|
|
|
|
def stop(self) -> None:
|
|
if self._state is not AppState.PAUSED:
|
|
raise InvalidStateUpdateError(f"Stop called but the current app state is {self._state}")
|
|
self._state = AppState.STOPPING
|
|
self._on_stop()
|
|
self.on_stop()
|
|
self._state = AppState.STOPPED
|
|
|
|
def _on_stop(self) -> None:
|
|
pass
|
|
|
|
def on_stop(self) -> None:
|
|
raise NotImplementedError("on_stop not implemented.")
|
|
|
|
def _update_lighting(self) -> None:
|
|
last_update_ago = monotonic_ns() - self._last_lighting_update
|
|
if last_update_ago > MAX_LIGHTING_UPDATE_RATE:
|
|
self._last_lighting_update = monotonic_ns()
|
|
new_brightness = self.config.brightness()
|
|
if self._current_brightness != new_brightness:
|
|
# Setting the brightness here
|
|
self._key_lights = [tuple(rgb_val * new_brightness / self._current_brightness for rgb_val in color) for
|
|
color in self._key_lights]
|
|
self._current_brightness = self.config.brightness()
|
|
for index, color in enumerate(self._key_lights):
|
|
self.macropad.pixels[index] = color
|
|
|
|
def add_displays_to_group(self) -> None:
|
|
self._display_group.append(self._title_label)
|
|
self._display_group.append(self._layout)
|
|
self.macropad.display.show(self._display_group)
|
|
|
|
def remove_displays_from_group(self):
|
|
self._display_group.remove(self._title_label)
|
|
self._display_group.remove(self._layout)
|
|
|
|
def set_color(self, x, y, color) -> None:
|
|
print("setting color")
|
|
color = tuple(rgb_val * self._current_brightness for rgb_val in color)
|
|
key_value = convert_to_keynum(x, y)
|
|
if key_value >= 12:
|
|
raise ValueError("color index out of range")
|
|
if len(color) != 3:
|
|
self._key_lights[key_value] = color
|
|
|
|
def set_colors(self, colors) -> None:
|
|
if len(colors) != 12:
|
|
raise ValueError("Colors must be passed in as a 12 len array")
|
|
for color in colors:
|
|
if len(color) != 3:
|
|
raise ValueError("Color format error - color must be length 3")
|
|
self._key_lights = [tuple(round(rgb_val * self._current_brightness / 100) for rgb_val in color) for color in
|
|
colors]
|
|
|
|
def get_colors(self) -> [(int, int, int)]:
|
|
return self._key_lights
|
|
|
|
def set_tone(self, keypad_num, tone) -> None:
|
|
if keypad_num >= 12:
|
|
raise ValueError("Tone index out of range")
|
|
if tone < 20 or tone > 20000:
|
|
raise ValueError("Tone format error - tone out of human hearing range (20 - 20000)")
|
|
self._key_tones[keypad_num] = tone
|
|
|
|
def set_tones(self, tones) -> None:
|
|
if len(tones) != 12:
|
|
raise ValueError("Tones must be passed in as a 12 len array")
|
|
for tone in tones:
|
|
if tone < 20 or tone > 20000:
|
|
raise ValueError("Tone format error - tone out of human hearing range (20 - 20000)")
|
|
for index, hz in enumerate(tones):
|
|
self._key_tones[index] = hz
|
|
|
|
def get_tones(self) -> {int: int}:
|
|
return self._key_tones
|
|
|
|
def set_title(self, title) -> None:
|
|
"""
|
|
:string title: Title of your app - shown in the top bar.
|
|
|
|
:return: None
|
|
"""
|
|
if len(title) > 22:
|
|
# TODO: Update this to be able to scroll the display if too long?
|
|
raise ValueError("Title too long to be displayed on screen")
|
|
self._title = title
|
|
diff = int((22 - len(title)) / 2)
|
|
self._title_label.text = f"{''.join([' ' for _ in range(diff)])}{self._title}{''.join([' ' for _ in range(diff)])}"
|
|
|
|
def set_layout(self, layout) -> None:
|
|
"""
|
|
:displayio.Group layout:
|
|
|
|
:return:
|
|
"""
|
|
self._layout = layout
|
|
|
|
def set_labels(self, labels) -> None:
|
|
self._labels = labels
|
|
# if self._layout
|
|
|
|
def press_macro(self, macro:Macro) -> None:
|
|
for item in macro.codes:
|
|
if callable(item):
|
|
item()
|
|
elif isinstance(item, int):
|
|
if item >= 0:
|
|
self.macropad.keyboard.press(item)
|
|
else:
|
|
self.macropad.keyboard.release(-item)
|
|
elif isinstance(item, float):
|
|
sleep(item)
|
|
elif isinstance(item, str):
|
|
self.macropad.keyboard_layout.write(item)
|
|
elif isinstance(item, list):
|
|
for code in item:
|
|
if isinstance(code, int):
|
|
self.macropad.consumer_control.release()
|
|
self.macropad.consumer_control.press(code)
|
|
if isinstance(code, float):
|
|
sleep(code)
|
|
elif isinstance(item, dict):
|
|
if 'buttons' in item:
|
|
if item['buttons'] >= 0:
|
|
self.macropad.mouse.press(item['buttons'])
|
|
else:
|
|
self.macropad.mouse.release(-item['buttons'])
|
|
self.macropad.mouse.move(item['x'] if 'x' in item else 0,
|
|
item['y'] if 'y' in item else 0,
|
|
item['wheel'] if 'wheel' in item else 0)
|
|
if 'tone' in item:
|
|
if item['tone'] > 0:
|
|
self.macropad.stop_tone()
|
|
self.macropad.start_tone(item['tone'])
|
|
else:
|
|
self.macropad.stop_tone()
|
|
elif 'play' in item:
|
|
self.macropad.play_file(item['play'])
|
|
|
|
def release_macro(self, macro:Macro) -> None:
|
|
for item in macro.codes:
|
|
if isinstance(item, int):
|
|
if item >= 0:
|
|
self.macropad.keyboard.release(item)
|
|
elif isinstance(item, dict):
|
|
if 'buttons' in item:
|
|
if item['buttons'] >= 0:
|
|
self.macropad.mouse.release(item['buttons'])
|
|
elif 'tone' in item:
|
|
self.macropad.stop_tone()
|
|
self.macropad.consumer_control.release()
|
|
release_action = macro.released
|
|
if release_action:
|
|
if callable(release_action):
|
|
release_action()
|
|
else:
|
|
self.keyboard.send(release_action)
|
|
|
|
def register_on_key_pressed(self, function) -> None:
|
|
self._key_pressed_callbacks.append(function)
|
|
|
|
def register_on_key_released(self, function) -> None:
|
|
self._key_released_callbacks.append(function)
|
|
|
|
def register_on_encoder_changed(self, function) -> None:
|
|
self._encoder_changed_callbacks.append(function)
|