diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..5741353 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,5 @@ +from .abstract_app import App +from .app_state import AppState, InvalidStateUpdateError +from .app_router import AppRouter +from .debug_app import DebugApp +from .options_app import OptionsApp \ No newline at end of file diff --git a/app/abstract_app.py b/app/abstract_app.py new file mode 100644 index 0000000..ab2338c --- /dev/null +++ b/app/abstract_app.py @@ -0,0 +1,82 @@ +import displayio + +from .app_state import AppState, InvalidStateUpdateError + + + +DISPLAY = displayio.Group() + +class App(object): + + def __init__(self, macropad, config): + print("app created") + self.macropad = macropad + self.config = config + self.name = "app" + self.state = AppState.STOPPED + self.display_group = DISPLAY + + def start(self): + 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): + pass + + def on_start(self): + raise NotImplementedError("on_start not implemented") + + def resume(self): + 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): + pass + + def on_resume(self): + raise NotImplementedError("on_resume not implemented") + + def pause(self): + 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): + self.macropad.keyboard.release_all() + self.macropad.consumer_control.release() + self.macropad.mouse.release_all() + self.macropad.stop_tone() + self.macropad.pixels.show() + # self.macropad.display.refresh() + + def on_pause(self): + raise NotImplementedError("on_pause not implemented") + + def loop(self): + raise NotImplementedError("Not implemented") + + def stop(self): + 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): + pass + + def on_stop(self): + raise NotImplementedError("on_stop not implemented.") diff --git a/app/app_router.py b/app/app_router.py new file mode 100644 index 0000000..f6b59e8 --- /dev/null +++ b/app/app_router.py @@ -0,0 +1,65 @@ +import time + +from .app_state import AppState +from .options_app import OptionsApp + + +class AppRouter(object): + def __init__(self, macropad, config, apps): + print("app router") + self.macropad = macropad + self.app_index = 0 + self.apps = apps + self.options = OptionsApp(macropad, config) + self.current_app = apps[self.app_index] + self.config = config + self.encoder_state = False + self.options_time = 1000000000 # .5 seconds in nanoseconds + self.click_time = 0 + + def swap_to_app(self, app): + print("Pausing current app") + self.current_app.pause() + print("Selecting new app") + self.current_app = app + if self.current_app.state is AppState.STOPPED: + print("Starting new app") + self.current_app.start() + if self.current_app.state is AppState.PAUSED: + print("Starting new app") + self.current_app.resume() + print(time.monotonic_ns()) + + def start(self): + self.current_app.start() + self.current_app.resume() + + while True: + # detect if the current app is what should be running + # stop current app and start new one if not + self.current_app.loop() + # any other finite state machine logic that comes up + if self.macropad.encoder_switch_debounced.pressed and not self.encoder_state: + self.macropad.play_tone(1000, .1) + self.encoder_state = True + self.click_time = time.monotonic_ns() + + elif self.macropad.encoder_switch_debounced.released and self.encoder_state: + self.encoder_state = False + if self.current_app is self.options: + print("Moving from options to the last opened app.") + else: + self.app_index += 1 + print("Moving to the next app on the list. ") + self.swap_to_app(self.apps[self.app_index % len(self.apps)]) + print("released encoder") + if self.encoder_state and self.click_time: + self.release_time = time.monotonic_ns() + print(self.release_time) + print(self.click_time) + print(self.release_time - self.click_time) + if (time.monotonic_ns() - self.click_time) > self.options_time: + self.macropad.play_tone(1000, .1) + self.swap_to_app(self.options) + self.encoder_state = False + self.macropad.encoder_switch_debounced.update() diff --git a/app/app_state.py b/app/app_state.py new file mode 100644 index 0000000..f015dd0 --- /dev/null +++ b/app/app_state.py @@ -0,0 +1,12 @@ +class InvalidStateUpdateError(Exception): + pass + + +class AppState(object): + STARTING = "starting" + RESUMING = "resuming" + RUNNING = "running" + PAUSING = "pausing" + PAUSED = "paused" + STOPPING = "stopping" + STOPPED = "stopped" diff --git a/app/debug_app.py b/app/debug_app.py new file mode 100644 index 0000000..c64672c --- /dev/null +++ b/app/debug_app.py @@ -0,0 +1,92 @@ +import terminalio +from adafruit_display_text import bitmap_label as label +from adafruit_displayio_layout.layouts.grid_layout import GridLayout +from rainbowio import colorwheel + +from .abstract_app import App + + +def rgb_from_int(rgb): + blue = rgb & 255 + green = (rgb >> 8) & 255 + red = (rgb >> 16) & 255 + return red, green, blue + + +class DebugApp(App): + + def __init__(self, macropad, config, name): + super().__init__(macropad, config) + self.name = name + self.tones = [196, 220, 246, 262, 294, 330, 349, 392, 440, 494, 523, 587] + self.wheel_offset = 0 + self.send_keyboard_inputs = 0 + self.lit_keys = [False] * 12 + self.labels = [] + self.layout = GridLayout(x=0, y=9, width=128, height=54, grid_size=(4, 4), cell_padding=1) + self.title = label.Label( + y=4, + font=terminalio.FONT, + color=0x0, + text=f" {self.name} MENU ", + background_color=0xFFFFFF, + ) + + def on_start(self): + print("on start from the app!") + self.lit_keys = [False] * 12 + self.macropad.display.show(self.display_group) + for _ in range(12): + self.labels.append(label.Label(terminalio.FONT, text="")) + + for index in range(12): + x = index % 3 + 1 + y = index // 3 + self.layout.add_content(self.labels[index], grid_position=(x, y), cell_size=(1, 1)) + + + def on_resume(self): + print("resume from the debug app!") + + print(id(self.display_group)) + print(self.display_group) + self.display_group.append(self.title) + self.display_group.append(self.layout) + self.macropad.display.show(self.display_group) + + def on_pause(self): + print("Pausing") + + self.display_group.remove(self.title) + self.display_group.remove(self.layout) + # self.macropad.display.show(None) + + + def on_stop(self): + print("Stopping") + + def loop(self): + self.process_key_presses() + self.light_keys() + + def process_key_presses(self): + key_event = self.macropad.keys.events.get() + if key_event: + if key_event.pressed: + self.labels[key_event.key_number].text = "KEY{}".format(key_event.key_number) + print(self.macropad.keys) + self.lit_keys[key_event.key_number] = not self.lit_keys[key_event.key_number] + self.macropad.stop_tone() + self.macropad.start_tone(self.tones[key_event.key_number]) + else: + self.labels[key_event.key_number].text = "" + self.macropad.stop_tone() + + def light_keys(self): + self.wheel_offset += 1 + for pixel in range(12): + if self.lit_keys[pixel]: + (r, g, b) = rgb_from_int(colorwheel((pixel / 12 * 256) + self.wheel_offset)) + self.macropad.pixels[pixel] = (r * .1, g * .1, b * .1) + else: + self.macropad.pixels[pixel] = 0 diff --git a/app/options_app.py b/app/options_app.py new file mode 100644 index 0000000..3c88fcd --- /dev/null +++ b/app/options_app.py @@ -0,0 +1,81 @@ +import terminalio +from adafruit_display_text import label +from adafruit_displayio_layout.layouts.grid_layout import GridLayout +from rainbowio import colorwheel + +from .abstract_app import App +from .debug_app import rgb_from_int + + +class OptionsApp(App): + + def __init__(self, macropad, config): + super().__init__(macropad, config) + self.tones = [196, 220, 246, 262, 294, 330, 349, 392, 440, 494, 523, 587] + self.wheel_offset = 0 + self.send_keyboard_inputs = 0 + self.lit_keys = [False] * 12 + self.labels = [] + self.layout = GridLayout(x=0, y=9, width=128, height=54, grid_size=(4, 1), cell_padding=1) + self.title = label.Label( + y=4, + font=terminalio.FONT, + color=0x0, + text=f" OPTIONS MENU ", + background_color=0xFFFFFF, + ) + + def on_start(self): + print("on start from the app!") + self.lit_keys = [False] * 4 + # self.macropad.display.show(self.display_group) + for _ in range(4): + self.labels.append(label.Label(terminalio.FONT, text="")) + + for index in range(4): + x = index % 3 + 1 + y = index // 3 + self.layout.add_content(self.labels[index], grid_position=(x, y), cell_size=(1, 1)) + + def on_resume(self): + print("resume from the options app!") + self.display_group.append(self.title) + self.display_group.append(self.layout) + self.macropad.display.show(self.display_group) + print(self.display_group) + print(id(self.display_group)) + + def on_pause(self): + print("Pausing") + self.display_group.remove(self.title) + self.display_group.remove(self.layout) + + def on_stop(self): + print("Stopping") + + def loop(self): + self.process_key_presses() + self.light_keys() + + def process_key_presses(self): + key_event = self.macropad.keys.events.get() + if key_event: + if key_event.key_number < 4: + if key_event.pressed : + self.labels[key_event.key_number].text = "KEY{}".format(key_event.key_number) + print(self.macropad.keys) + self.lit_keys[key_event.key_number] = not self.lit_keys[key_event.key_number] + self.macropad.stop_tone() + self.macropad.start_tone(self.tones[key_event.key_number]) + else: + self.labels[key_event.key_number].text = "" + self.macropad.stop_tone() + + def light_keys(self): + self.wheel_offset += 1 + for pixel in range(4): + if self.lit_keys[pixel]: + (r, g, b) = rgb_from_int(colorwheel((pixel / 4 * 256) + self.wheel_offset)) + self.macropad.pixels[pixel] = (r * .1, g * .1, b * .1) + else: + self.macropad.pixels[pixel] = 0 diff --git a/boot.py b/boot.py new file mode 100644 index 0000000..1a75c46 --- /dev/null +++ b/boot.py @@ -0,0 +1,16 @@ +import storage + +from config import Config, ConfigVars + +config = Config("config.json") + +print("test") +print(config.data) + +if not config.data.get(ConfigVars.DEV_MODE.name): + storage.disable_usb_drive() + print("file system should be writable") + storage.remount("/", False) + config.data[ConfigVars.DEV_MODE.name] = True + config.save() + print("success") diff --git a/code.py b/code.py new file mode 100644 index 0000000..c1ddb70 --- /dev/null +++ b/code.py @@ -0,0 +1,17 @@ +import displayio + +from app import AppRouter, DebugApp +from config import Config +from adafruit_macropad import MacroPad + +macropad = MacroPad() +config = Config("config.json") + + +ar = AppRouter(macropad, config, [ + DebugApp(macropad, config, "debug 1"), + # DebugApp(macropad, config, "debug 2"), + # DebugApp(macropad, config, "debug 3") +]) + +ar.start() diff --git a/config.json b/config.json new file mode 100644 index 0000000..b0f8073 --- /dev/null +++ b/config.json @@ -0,0 +1,4 @@ +{ + "dev_mode": true, + "brightness": 100 +} \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..2313cb2 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,2 @@ +from .config_vars import ConfigVars, ConVar +from .config import Config diff --git a/config/config.py b/config/config.py new file mode 100644 index 0000000..dd53fb5 --- /dev/null +++ b/config/config.py @@ -0,0 +1,20 @@ +import json + +from .config_vars import ConfigVars + + +class Config(object): + def __init__(self, save_file): + self.data = {} + self.save_file = save_file + self.load() + + def load(self): + with open(self.save_file, "r") as f: + self.data = json.load(f) + for convar in ConfigVars.__dict__.keys(): + print(convar) + + def save(self): + with open(self.save_file, "w") as f: + json.dump(self.data, f) diff --git a/config/config_vars.py b/config/config_vars.py new file mode 100644 index 0000000..9bd9e37 --- /dev/null +++ b/config/config_vars.py @@ -0,0 +1,9 @@ +class ConVar(object): + def __init__(self, name, default): + self.name = name + self.default = default + + +class ConfigVars(object): + DEV_MODE = ConVar("dev_mode", True) + TEST_VAR = ConVar("test_var", False) diff --git a/copy_to_device.cmd b/copy_to_device.cmd new file mode 100644 index 0000000..6aa282a --- /dev/null +++ b/copy_to_device.cmd @@ -0,0 +1 @@ +xcopy .\* E:\ /s /d /exclude:excludedfileslist.txt /Y \ No newline at end of file diff --git a/excludedfileslist.txt b/excludedfileslist.txt new file mode 100644 index 0000000..96d6026 --- /dev/null +++ b/excludedfileslist.txt @@ -0,0 +1 @@ +.\.idea\ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..05e541b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +adafruit_circuitpython_debouncer +adafruit_circuitpython_macropad +adafruit_circuitpython_pixelbuf +adafruit_circuitpython_simple_text_display +adafruit_circuitpython_ticks +neopixel +adafruit_circuitpython_bitmap_font +adafruit_circuitpython_display_text +adafruit_circuitpython_hid +adafruit_circuitpython_midi +adafruit_circuitpython_display_shapes +adafruit_circuitpython_displayio_layout +adafruit_circuitpython_imageload