Compare commits

..

10 Commits

Author SHA1 Message Date
Lucas Oskorep d598def3fb Update LICENSE copyright to Lucas Oskorep 2026-02-27 19:14:17 -05:00
Lucas Oskorep 57dda76a4e Add LICENSE.txt with GPL-3.0-only license 2026-02-27 19:12:15 -05:00
Lucas Oskorep 1cc9348d24 feat: Adding support for json apps. Updated Macropad_os.py to automatically load in the python and json apps at launch. 2022-09-20 12:56:41 -04:00
Lucas Oskorep 6724f8a4f3 Added Numpad V1.0 and added in support for automatically pressing and releasing macros to the abstract app 2022-09-20 11:18:45 -04:00
Lucas Oskorep cb4dac9d6b adding macro_utils and then implementing them in the default numpad app. 2022-09-20 01:38:32 -04:00
Lucas Oskorep 44aaa083cc a 2022-09-20 00:49:43 -04:00
Lucas Oskorep 26ce2d1734 feat: Added automatic loading of Apps from the macropad_apps/python directory at load time.
Performance improvements to the lighting system.
Renamed app_router to macropad_os.
2022-09-20 00:44:42 -04:00
Lucas Oskorep d95d351bfd fixing issue with imports and changing direction for scrolling with click wheel. 2022-09-19 23:23:59 -04:00
Lucas Oskorep 3a72054866 ascii art 2022-09-19 23:15:19 -04:00
Lucas Oskorep ce61de7a6f Adding 3d models to grouped folders and adding a readme for creating and installing macropadOS 2022-09-19 23:10:47 -04:00
24 changed files with 666 additions and 203 deletions
+1
View File
@@ -116,3 +116,4 @@ dmypy.json
.idea*
config.json
+124
View File
@@ -0,0 +1,124 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software.
The licenses for most software are designed to take away your freedom to share and change the works.
By contrast, the GPL is intended to guarantee your freedom to share and change all versions.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender them.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software.
Some devices are designed to deny users access to install or run modified versions of the software inside them.
This is fundamentally incompatible with the aim of protecting users' freedom to change software.
Finally, every program is threatened constantly by software patents.
The GPL assures that patents cannot be used to render the program non-free.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of works.
"The Program" refers to any copyrightable work licensed under this License.
1. Source Code.
The "source code" for a work means the preferred form for making modifications.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program,
and are irrevocable provided the stated conditions are met.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it.
5. Conveying Modified Source Versions.
You may convey a work based on the Program under section 4, provided that you also meet all conditions:
a) The work must carry prominent notices stating that you modified it.
b) You must license the entire work under this License.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under sections 4 and 5, provided that you also convey
the machine-readable Corresponding Source under the terms of this License.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License by making exceptions.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License of the Program.
12. No Surrender of Others' Freedom.
If conditions are imposed on you that contradict the conditions of this License, they do not excuse you from the conditions.
13. Use with the GNU Affero General Public License.
You have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER BE LIABLE TO YOU FOR DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public,
the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program:
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> Lucas Oskorep
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
========================================================================
Copyright (C) YOUR NAME 2024
Licensed under the GNU General Public License Version 3 (GPL-3.0-only).
This program is free software: you can redistribute it and/or modify
it under the terms of the GPL v3 as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
See <https://www.gnu.org/licenses/> for full license text.
+76 -14
View File
@@ -1,28 +1,90 @@
```
_______________________
| | | / \ | Macropad OS
| |_________| \____/ |
| |
_______________________
| |MACROPAD OS!| / \ |
| |____________| \____/ |
| _____ _____ _____ |
||key1 | |key2 | |key3 ||
||_____| |_____| |_____||
| _____ _____ _____ |
||key4 | |key5 | |key6 ||
||_____| |_____| |_____||
| _____ _____ _____ |
||key7 | |key8 | |key9 ||
||_____| |_____| |_____||
| _____ _____ _____ | __ ___ __ ____ _____
||key4 | |key5 | |key6 || / \/ /___ _______________ ____ ____ _____/ / / __ \/ ___/
||_____| |_____| |_____|| / /|_/ / __ `/ ___/ ___/ __ \/ __ \/ __ `/ __ / / / / /\__ \
| _____ _____ _____ | / / / / /_/ / /__/ / / /_/ / /_/ / /_/ / /_/ / / /_/ /___/ /
||key7 | |key8 | |key9 || /_/ /_/\__,_/\___/_/ \____/ .___/\__,_/\__,_/ \____//____/
||_____| |_____| |_____|| /_/
| _____ _____ _____ |
||key11| |key13| |key12||
||_____| |_____| |_____||
|_______________________|
```
What is Macropad OS?
# Macropad OS!
MacropadOS is a simple OS build to run on the [Adafruit Macropad](https://www.adafruit.com/product/5128). It's a very
basic GUI running in CircuitPython which allows users to navigate through apps, change settings, and more coming soon!
The OS itself is one part of the project though, this also includes a framework for building quick and simple apps in
CircuitPython using a framework very similar to Android apps with a very similar app-lifecycle (OnStart, OnResume, etc).
This repo contains everything you need to get started, from building the macropad itself to installing the software and
creating your own apps!
How to Build:
## Building the Macropad :
Pictures and a more complete build guide coming soon<sup>tm</sup>
See the 3d_printing_files folder for all the required .3mf model files!
If printing at home is not an option for you, you can reach out to https://craftcloud3d.com or similar services to
print the files for you!
How to Install:
* Connect your complete
#### Required Materials:
* 3d Printed
* 1x Main body
* 1x Key stabilizer
* 2x Peg Feet (can be TPU)
* 1x Support Bar (can be TPU)
* 12x Cherry MX Compatible KeyCaps
* Purchased
* 12x Cherry MX Compatible Keys
#### Optional (also 3d printed, but with TPU or another flex material):
* 1x Top Dampener
* 1x Bottom Dampener
#### Assembly:
1. Print all the required parts and any optional parts above.
2. (Optional)Take the dampeners if you are using them and place them into the main body with the pegs facing out of the body at the top and bottom.
3. Take the Adafruit Macropad and place it into the main body. It will snap into the second set of slots on the side of the body.
4. Snap the key stabilizer into the top slots on the body above the macropad itself. Feet of the stabilizer should touch the macropad itself.
5. Install the 12 keys into each slot
6. Add the Keycaps and press them onto each key
7. Flip macropad over and press in peg feet
8. Do the same for the top Support bar
9. You now have a fully assembled Macropad!
## Installing Macropad OS:
Requirements:
* Macropad with the CircuitPython installed on it. See [docs](https://CircuitPython.org/board/adafruit_macropad_rp2040/)
* Python 3.5+
Installation:
```bash
pip install circup # See the circup repository(https://github.com/adafruit/circup) for any issues installing circup
circup install -r requirements.txt
# Open copy_to_device.cmd and make the first line the letter of your circutpython drive
copy_to_device.cmd
# Alternatively just copy all of the files not in the 3d printing folder and copy them to the CircuitPython device
```
Congrats! MacropadOS is now installed and after all of the files are transferred to the device it will boot.
## Creating JSON Apps (Easy):
Documentation coming soon...
## Creating Python Apps (Advanced):
Documentation coming soon...
+12 -11
View File
@@ -1,22 +1,23 @@
from macropad_os import AppRouter, SerialComms, Config
import os
from macropad_os import MacropadOS, SerialComms, Config, App
from adafruit_macropad import MacroPad
from macropad_apps.python import NumpadApp
macropad = MacroPad()
default_config = Config("default_config.json").load()
config = Config("config.json").load(default_config)
ar = AppRouter(macropad, config, [
NumpadApp(macropad, config),
# Arrow Keys
# Script Runner
])
sc = SerialComms(config)
PYTHON_APP_FOLDER = "./macropad_apps/python"
JSON_APP_FOLDER = "./macropad_apps/json"
ar = MacropadOS(
macropad,
config,
python_apps=PYTHON_APP_FOLDER,
json_apps=JSON_APP_FOLDER
)
# sc = SerialComms(config)
# _thread.start_new_thread(sc.run, (sc))
ar.start()
-54
View File
@@ -1,54 +0,0 @@
{
"profileName" : "",
"sortOrder" : 99,
"key1" : {
"text" : [""],
"value" : [""]
},
"key2" : {
"text" : [""],
"value" : [""]
},
"key3" : {
"text" : [""],
"value" : [""]
},
"key4" : {
"text" : [""],
"value" : [""]
},
"key5" : {
"text" : [""],
"value" : [""]
},
"key6" : {
"text" : [""],
"value" : [""]
},
"key7" : {
"text" : [""],
"value" : [""]
},
"key8" : {
"text" : [""],
"value" : [""]
},
"key9" : {
"text" : [""],
"value" : [""]
},
"key10" : {
"text" : [""],
"value" : [""]
},
"key11" : {
"text" : [""],
"value" : [""]
},
"key12" : {
"text" : [""],
"value" : [""]
}
}
+25 -26
View File
@@ -1,54 +1,53 @@
{
"profileName" : "Gamepad",
"profileName" : "Gamepad Json",
"sortOrder" : 1,
"key1" : {
"text" : ["ESC"],
"value" : ["ESCAPE"]
"text" : "ESC",
"values" : ["ESCAPE"]
},
"key2" : {
"text" : ["Tab"],
"value" : ["TAB"]
"text" : "Tab",
"values" : ["TAB"]
},
"key3" : {
"text" : ["R"],
"value" : ["R"]
"text" : "R",
"values" : ["R"]
},
"key4" : {
"text" : ["Q"],
"value" : ["Q"]
"text" : "Q",
"values" : ["Q"]
},
"key5" : {
"text" : ["W"],
"value" : ["W"]
"text" : "W",
"values" : ["W"]
},
"key6" : {
"text" : ["E"],
"value" : ["E"]
"text" : "E",
"values" : ["E"]
},
"key7" : {
"text" : ["A"],
"value" : ["A"]
"text" : "A",
"values" : ["A"]
},
"key8" : {
"text" : ["S"],
"value" : ["S"]
"text" : "S",
"values" : ["S"]
},
"key9" : {
"text" : ["D"],
"value" : ["D"]
"text" : "D",
"values" : ["D"]
},
"key10" : {
"text" : ["Shift"],
"value" : [""]
"text" : "Shift",
"values" : ["SHIFT"]
},
"key11" : {
"text" : ["C"],
"value" : ["C"]
"text" : "C",
"values" : ["C"]
},
"key12" : {
"text" : ["Space"],
"value" : ["SPACEBAR"]
"text" : "Space",
"values" : ["SPACEBAR"]
}
}
+25 -26
View File
@@ -1,53 +1,52 @@
{
"profileName" : "NumPad",
"profileName" : "Numpad Json",
"sortOrder" : 0,
"key1" : {
"text" : ["7"],
"value" : ["SEVEN"]
"text" : "7",
"values" : ["SEVEN"]
},
"key2" : {
"text" : ["8"],
"value" : ["EIGHT"]
"text" : "8",
"values" : ["EIGHT"]
},
"key3" : {
"text" : ["9"],
"value" : ["NINE"]
"text" : "9",
"values" : ["NINE"]
},
"key4" : {
"text" : ["4"],
"value" : ["FOUR"]
"text" : "4",
"values" : ["FOUR"]
},
"key5" : {
"text" : ["5"],
"value" : ["FIVE"]
"text" : "5",
"values" : ["FIVE"]
},
"key6" : {
"text" : ["6"],
"value" : ["SIX"]
"text" : "6",
"values" : ["SIX"]
},
"key7" : {
"text" : ["1"],
"value" : ["ONE"]
"text" : "1",
"values" : ["ONE"]
},
"key8" : {
"text" : ["2"],
"value" : ["TWO"]
"text" : "2",
"values" : ["TWO"]
},
"key9" : {
"text" : ["3"],
"value" : ["THREE"]
"text" : "3",
"values" : ["THREE"]
},
"key10" : {
"text" : ["0"],
"value" : ["ZERO"]
"text" : "0",
"values" : ["ZERO"]
},
"key11" : {
"text" : ["."],
"value" : ["PERIOD"]
"text" : ".",
"values" : ["PERIOD"]
},
"key12" : {
"text" : ["ENT"],
"value" : ["RETURN"]
"text" : "ENT",
"values" : ["RETURN"]
}
}
+54
View File
@@ -0,0 +1,54 @@
{
"profileName" : "",
"sortOrder" : 99,
"key1" : {
"text" : "",
"values" : [""]
},
"key2" : {
"text" : "",
"values" : [""]
},
"key3" : {
"text" : "",
"values" : [""]
},
"key4" : {
"text" : "",
"values" : [""]
},
"key5" : {
"text" : "",
"values" : [""]
},
"key6" : {
"text" : "",
"values" : [""]
},
"key7" : {
"text" : "",
"values" : [""]
},
"key8" : {
"text" : "",
"values" : [""]
},
"key9" : {
"text" : "",
"values" : [""]
},
"key10" : {
"text" : "",
"values" : [""]
},
"key11" : {
"text" : "",
"values" : [""]
},
"key12" : {
"text" : "",
"values" : [""]
}
}
-1
View File
@@ -1 +0,0 @@
from .numpad import NumpadApp
+54 -35
View File
@@ -1,47 +1,67 @@
from time import monotonic_ns
import terminalio
from adafruit_display_text.bitmap_label import Label
from adafruit_display_text.scrolling_label import ScrollingLabel
from adafruit_displayio_layout.layouts.grid_layout import GridLayout
from adafruit_hid.keycode import Keycode
from rainbowio import colorwheel
from macropad_os import App
from macropad_os.app_utils import rgb_from_int
from macropad_os.app_utils import rgb_from_int, MacroSet, Macro
labels = [
"7", "8", "9",
"4", "5", "6",
"1", "2", "3",
"0", ".", "SHIFT"
]
modified_labels = [
"<", ">", "&",
"(", ")", "%",
"/", "+", "-",
"*", "=", "SHIFT"
]
COLOR_UPDATE_RATE = 33000000 # .033 seconds
class NumpadApp(App):
def __init__(self, macropad, config):
super().__init__(macropad, config)
self.name = "Numpad"
self.name = "NumpadApp"
self.wheel_offset = 0
self.lit_keys = [False] * 12
self.labels = []
self.title = "Numpad"
self.modifier_pressed = False
self.last_color_update = 0
self.macros = MacroSet(
[
Macro("7", Keycode.SEVEN), Macro("8", Keycode.EIGHT), Macro("9", Keycode.NINE),
Macro("4", Keycode.FOUR), Macro("5", Keycode.FIVE), Macro("6", Keycode.SIX),
Macro("1", Keycode.ONE), Macro("2", Keycode.TWO), Macro("3", Keycode.THREE),
Macro("0", Keycode.ZERO), Macro(".", Keycode.PERIOD),
Macro("Mod", self.swap_modifier, released=self.swap_modifier)
],
encoder_up=Macro("+", Keycode.KEYPAD_PLUS),
encoder_down=Macro("-", Keycode.KEYPAD_MINUS),
)
self.mod_macros = MacroSet([
Macro("<", Keycode.SHIFT, Keycode.COMMA), Macro(">", Keycode.SHIFT, Keycode.PERIOD), Macro("BACKSP", Keycode.BACKSPACE),
Macro("(", Keycode.SHIFT, Keycode.NINE), Macro(")", Keycode.SHIFT, Keycode.ZERO),Macro("%", Keycode.SHIFT, Keycode.FIVE),
Macro("/", Keycode.FORWARD_SLASH), Macro("+", Keycode.KEYPAD_PLUS), Macro("-", Keycode.MINUS),
Macro("*", Keycode.SHIFT, Keycode.EIGHT), Macro("=", Keycode.EQUALS), Macro("Mod", self.swap_modifier, released=self.swap_modifier)
],
encoder_up=Macro("+", Keycode.KEYPAD_PLUS),
encoder_down=Macro("-", Keycode.KEYPAD_MINUS),
)
self.active_macros = self.macros
def swap_modifier(self):
self.modifier_pressed = not self.modifier_pressed
if self.modifier_pressed:
self.active_macros = self.mod_macros
else:
self.active_macros = self.macros
for i in range(12):
self.labels[i].text = self.active_macros.get_macro_from_key(i).name
def on_start(self):
print("on start from the app!")
self.set_layout(GridLayout(x=0, y=9, width=128, height=54, grid_size=(3, 4), cell_padding=1))
self.set_title(self.title)
self.lit_keys = [True] * 12
for i in range(12):
self.labels.append(Label(terminalio.FONT, text=labels[i]))
self.labels.append(Label(terminalio.FONT, text=self.active_macros.get_macro_from_key(i).name))
for index in range(12):
x = index % 3
y = index // 3
@@ -65,21 +85,20 @@ class NumpadApp(App):
def update_key_colors(self):
self.wheel_offset += 1
colors = []
last_update_ago = monotonic_ns() - self.last_color_update
if last_update_ago > COLOR_UPDATE_RATE:
self.last_color_update = monotonic_ns()
for pixel in range(12):
if self.lit_keys[pixel]:
(r, g, b) = rgb_from_int(colorwheel((pixel / 12 * 256) + self.wheel_offset))
colors.append((r, g, b))
else:
colors.append((0, 0, 0))
(r, g, b) = rgb_from_int(colorwheel((pixel / 12 * 256) + self.wheel_offset))
colors.append((r, g, b))
self.set_colors(colors)
def process_keys_pressed_callback(self, keyevent):
print("PROCESS KEYS CALLBACK FROM DEBUG")
print(keyevent)
def process_keys_pressed_callback(self, key_event):
self.press_macro(self.active_macros.get_macro_from_key(key_event))
def process_keys_released_callback(self, keyevent):
print("PROCESS KEYS RELEASED CALLBACK FROM DEBUG")
print(keyevent)
def process_enbcoder_changed(self, keyevent):
print("PROCESS Encoder Changed Callback FROM DEBUG")
print(keyevent)
def process_keys_released_callback(self, key_event):
self.release_macro(self.active_macros.get_macro_from_key(key_event))
def process_enbcoder_changed(self, key_event):
print(key_event)
+1 -1
View File
@@ -2,4 +2,4 @@ from .serial_communications import SerialComms
from .configuration import Config, ConfigItem
from .app_state import AppState, InvalidStateUpdateError
from .abstract_app import App
from .app_router import AppRouter
from .macropad_os import MacropadOS
+88 -24
View File
@@ -1,10 +1,11 @@
import time
import displayio
import terminalio
from adafruit_display_text import label
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()
@@ -13,8 +14,12 @@ def convert_to_keynum(x, y):
return 3 * x + y
class App(object):
# 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):
"""
@@ -42,10 +47,12 @@ class App(object):
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:
@@ -86,7 +93,7 @@ class App(object):
self._state = AppState.PAUSED
def _on_pause(self) -> None:
self.macropad.keyboard.release_all()
self.keyboard.release_all()
self.macropad.consumer_control.release()
self.macropad.mouse.release_all()
self.macropad.stop_tone()
@@ -116,7 +123,6 @@ class App(object):
if key_event.key_number < 12:
if key_event.pressed:
self.macropad.stop_tone()
print(self.config.get_items())
self._play_tone_for_key(key_event.key_number)
if self._key_pressed_callbacks:
for callback in self._key_pressed_callbacks:
@@ -124,7 +130,7 @@ class App(object):
else:
self._stop_tone_for_key(key_event.key_number)
if self._key_released_callbacks:
for callback in self._key_pressed_callbacks:
for callback in self._key_released_callbacks:
callback(key_event.key_number)
def _play_tone_for_key(self, key_number):
@@ -137,12 +143,13 @@ class App(object):
def _stop_tone_for_key(self, key_number):
self.macropad.stop_tone()
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())
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
@@ -169,16 +176,17 @@ class App(object):
raise NotImplementedError("on_stop not implemented.")
def _update_lighting(self) -> None:
new_brightness = self.config.brightness()
if self._current_brightness != new_brightness:
print("SETTING BRIGHTNESS!!!!")
print(self._key_lights)
self._key_lights = [tuple(rgb_val * new_brightness / self._current_brightness for rgb_val in color) for
color in self._key_lights]
print(self._key_lights)
self._current_brightness = self.config.brightness()
for index, color in enumerate(self._key_lights):
self.macropad.pixels[index] = color
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)
@@ -199,7 +207,6 @@ class App(object):
self._key_lights[key_value] = color
def set_colors(self, colors) -> None:
print(time.time())
if len(colors) != 12:
raise ValueError("Colors must be passed in as a 12 len array")
for color in colors:
@@ -255,6 +262,63 @@ class App(object):
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)
+2 -2
View File
@@ -1,3 +1,3 @@
from .static_lighting import *
from .sound_patterns import *
from .helpers import *
from .helpers import *
from .macro_utils import *
+20
View File
@@ -0,0 +1,20 @@
class Macro(object):
def __init__(self, name, *codes, released = None):
self.name = name
self.codes = codes
self.released = released
class MacroSet(object):
def __init__(self, key_macros: [Macro], encoder_up: Macro, encoder_down: Macro):
if len(key_macros) != 12:
raise ValueError("12 keys must be passed to a keyset")
self._key_macros = key_macros
self.encoder_up = encoder_up
self.encoder_down = encoder_down
def get_macro_from_key(self, key_index) -> Macro:
if key_index < 0 or key_index > 11:
raise ValueError("Invalid key index")
return self._key_macros[key_index]
@@ -1,23 +1,29 @@
import time
import time, os
from . import App
from .app_state import AppState
from macropad_os.system_apps import OptionsApp, DebugApp
from macropad_os.system_apps import OptionsApp, DebugApp, JsonApp
class AppRouter(object):
def __init__(self, macropad, config, apps):
class MacropadOS(object):
def __init__(self, macropad, config, python_apps: str, json_apps: str):
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 = 500000000 # .5 seconds in nanoseconds
self.click_time = 0
self.debug_app = DebugApp(macropad, config, "DEBUG APP")
self.debug_app_active = self.config.debug_app_enabled()
self.python_apps_location = python_apps
self.json_apps_location = json_apps
self.apps: [App] = []
self.load_python_apps()
self.load_json_apps()
self.current_app = self.apps[self.app_index]
if self.debug_app_active:
self.apps.append(self.debug_app)
@@ -38,7 +44,41 @@ class AppRouter(object):
print("Starting new app")
self.current_app.resume()
def load_python_apps(self):
app_classes = []
for filename in sorted(os.listdir(self.python_apps_location)):
if filename.endswith('.py') and not filename.startswith('._'):
try:
print(filename)
module = __import__(self.python_apps_location + '/' + filename[:-3])
classes = [getattr(module, a) for a in dir(module)
if isinstance(getattr(module, a), type)]
for cls in classes:
if issubclass(cls, App) and cls.__name__ != "App":
app_classes.append(cls)
print(app_classes)
except (SyntaxError, ImportError, AttributeError, KeyError, NameError, IndexError, TypeError) as err:
print("ERROR in", filename)
import traceback
traceback.print_exception(err, err, err.__traceback__)
self.apps.extend(a(self.macropad, self.config) for a in app_classes)
def load_json_apps(self):
json_apps = []
for filename in sorted(os.listdir(self.json_apps_location)):
if filename.endswith('.json'):
json_apps.append(
JsonApp(
self.macropad,
self.config,
self.json_apps_location + "/" + filename
)
)
json_apps.sort(key=lambda x: x.sort_order)
self.apps.extend(json_apps)
def start(self) -> None:
print(self.current_app)
self.current_app.start()
self.current_app.resume()
+2 -1
View File
@@ -1,2 +1,3 @@
from .debug_app import DebugApp
from .options_app import OptionsApp
from .options_app import OptionsApp
from .json_app import JsonApp
+134
View File
@@ -0,0 +1,134 @@
import terminalio
import json
from time import monotonic_ns
from adafruit_display_text.bitmap_label import Label
from adafruit_displayio_layout.layouts.grid_layout import GridLayout
from adafruit_hid.keycode import Keycode
from macropad_os import App
from macropad_os.app_utils import rgb_from_int, Macro, MacroSet
from rainbowio import colorwheel
COLOR_UPDATE_RATE = 33000000 # .033 seconds
JSON_FILE_LOCATION = "./macropad_apps/json/"
PROFILE_NAME = "profileName"
SORT_ORDER = "sortOrder"
KEY_KEYS = ["key1", "key2", "key3", "key4", "key5", "key6", "key7", "key8", "key9", "key10", "key11", "key12"]
REQUIRED_KEYS = KEY_KEYS.copy()
REQUIRED_KEYS.extend(x for x in [PROFILE_NAME, SORT_ORDER])
class JsonApp(App):
def __init__(self, macropad, config, json_file):
super().__init__(macropad, config)
self.json_file = json_file
self.wheel_offset = 0
self.labels = []
self.title = ""
self.last_color_update = 0
self.active_macro = 0
self.name = "JSON_APP"
self.json_dict = JsonApp.read_json_file(self.json_file)
self.macros = JsonApp.get_macroset_from_dict(self.json_dict)
self.title = self.json_dict[PROFILE_NAME]
self.sort_order = self.json_dict[SORT_ORDER]
@staticmethod
def create_macro_from_dict(key_dict) -> Macro:
print(f"Creating macro for {key_dict}")
name = key_dict["text"]
values = key_dict["values"]
codes = []
for value in values:
if isinstance(value, str):
value_mod = 1
if value[0] is '-':
value_mod = -1
value = value[1:]
if hasattr(Keycode, value):
keycode = getattr(Keycode, value)
codes.append(keycode * value_mod)
else:
codes.append(value)
if isinstance(value, int) or isinstance(value, float) or isinstance(value, dict) or isinstance(value, list):
# TODO: Add validation for lists and dictionaries
codes.append(value)
print(name, codes)
return Macro(name, *codes)
@staticmethod
def get_macroset_from_dict(json_dict: {}) -> MacroSet:
key_macros = []
for key in KEY_KEYS:
key_macros.append(JsonApp.create_macro_from_dict(json_dict[key]))
return MacroSet(key_macros=key_macros, encoder_up=Macro("Test"), encoder_down=Macro("Test"))
@staticmethod
def read_json_file(json_file) -> {}:
try:
f = open(json_file, "r")
json_dict = json.load(f)
print(json_dict)
for key in REQUIRED_KEYS:
if key not in json_dict:
raise ValueError(f"JSON FOR FILE - {json_file} IS INVALID - KEY MISSING {key}")
return json_dict
except Exception as e:
print(e)
def on_start(self):
print("on start from the app!")
self.set_layout(GridLayout(x=0, y=9, width=128, height=54, grid_size=(3, 4), cell_padding=1))
self.set_title(self.title)
for i in range(12):
self.labels.append(
Label(terminalio.FONT, text=self.macros.get_macro_from_key(i).name))
for index in range(12):
x = index % 3
y = index // 3
self._layout.add_content(self.labels[index], grid_position=(x, y), cell_size=(1, 1))
self.register_on_key_pressed(self.process_keys_pressed_callback)
self.register_on_key_released(self.process_keys_released_callback)
self.register_on_encoder_changed(self.process_enbcoder_changed)
def on_resume(self):
print("resume from the debug app!")
def on_pause(self):
print("Pausing")
def on_stop(self):
print("Stopping")
def on_loop(self):
self.update_key_colors()
def update_key_colors(self):
self.wheel_offset += 1
colors = []
last_update_ago = monotonic_ns() - self.last_color_update
if last_update_ago > COLOR_UPDATE_RATE:
self.last_color_update = monotonic_ns()
for pixel in range(12):
(r, g, b) = rgb_from_int(colorwheel((pixel / 12 * 256) + self.wheel_offset))
colors.append((r, g, b))
self.set_colors(colors)
def process_keys_pressed_callback(self, key_event):
print(f"KEY PRESSED - {key_event}")
print(self.macros)
macro = self.macros.get_macro_from_key(key_event)
print(macro)
print(macro.name)
print(macro.codes)
self.press_macro(self.macros.get_macro_from_key(key_event))
def process_keys_released_callback(self, key_event):
self.release_macro(self.macros.get_macro_from_key(key_event))
def process_enbcoder_changed(self, key_event):
print(key_event)
+2 -2
View File
@@ -204,7 +204,7 @@ class OptionsApp(App):
def on_wheel_change(self, event):
if event > 0:
self._up()
else:
self._down()
else:
self._up()
self.update_settings_menu()