├── .gitignore ├── img ├── internals.JPG └── macropad.JPG ├── boot.py ├── README.md ├── pico_macropad ├── encoder.py └── hid.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /img/internals.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karolis92/pico-macropad/HEAD/img/internals.JPG -------------------------------------------------------------------------------- /img/macropad.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Karolis92/pico-macropad/HEAD/img/macropad.JPG -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | from digitalio import DigitalInOut, Direction, Pull 2 | import board 3 | import storage 4 | import usb_cdc 5 | 6 | 7 | def main(): 8 | firstRow = DigitalInOut(board.GP18) 9 | firstRow.direction = Direction.INPUT 10 | firstRow.pull = Pull.DOWN 11 | 12 | firstColumn = DigitalInOut(board.GP19) 13 | firstColumn.direction = Direction.OUTPUT 14 | firstColumn.value = True 15 | 16 | # disable mass storage and serial connection if first button is not pressed on boot 17 | if not firstRow.value: 18 | storage.disable_usb_drive() 19 | usb_cdc.disable() 20 | 21 | firstRow.deinit() 22 | firstColumn.deinit() 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Pico Macropad 2 | 3 | This repository contains CircuitPython code for macropad 4 | ## Usage 5 | 6 | In your CIRCUITPY drive: 7 | * Add these dependencies to the `lib` folder: 8 | * `adafruit_hid` from [CircuitPython bundle](https://circuitpython.org/libraries). 9 | * Copy `pico_macropad` to the root of the drive. 10 | * Create `main.py` and write your code there. Example can be found in this repo. 11 | * Optionally create `boot.py`. Example that disables usb drive and serial communication if first button in first row is not pressed on boot can be found in this repo. Be careful as that can potentially lock you out from accessing your microcontroller if pins are not configured correctly ([here](https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html#resetting-flash-memory) are flash reset instructions just in case). 12 | 13 | ## Wiring 14 | 15 | I used matrix keyboard with diodes. Instructions from Adafruit can be found [here](https://learn.adafruit.com/key-pad-matrix-scanning-in-circuitpython/keymatrix). 16 | 17 | ## Case 18 | 19 | STL files for printing can be found [here](https://www.printables.com/model/152449-raspberry-pi-pico-macropad). 20 | 21 | ## Photos 22 | 23 | ![macropad](/img/macropad.JPG) 24 | ![macropad internals](/img/internals.JPG) 25 | -------------------------------------------------------------------------------- /pico_macropad/encoder.py: -------------------------------------------------------------------------------- 1 | from digitalio import DigitalInOut, Direction, Pull 2 | from rotaryio import IncrementalEncoder 3 | from microcontroller import Pin 4 | 5 | 6 | class EncoderBtnEvent: 7 | def __init__(self, pressed: int): 8 | self.pressed = pressed 9 | 10 | 11 | class EncoderPostionEvent: 12 | def __init__(self, difference: int): 13 | self.difference = difference 14 | 15 | 16 | class Encoder: 17 | def __init__(self, pin_a: Pin, pin_b: Pin, pin_btn: Pin, divisor: int): 18 | self._enc = IncrementalEncoder(pin_a, pin_b, divisor) 19 | 20 | self._btn = DigitalInOut(pin_btn) 21 | self._btn.direction = Direction.INPUT 22 | self._btn.pull = Pull.UP 23 | 24 | self.last_btn_val = self._btn.value 25 | self.last_position = self.position 26 | 27 | @property 28 | def position(self): 29 | return self._enc.position 30 | 31 | def getButtonEvent(self): 32 | if self._btn.value != self.last_btn_val: 33 | self.last_btn_val = self._btn.value 34 | return EncoderBtnEvent(not self.last_btn_val) 35 | return None 36 | 37 | def getPositionEvent(self): 38 | diff = self.position - self.last_position 39 | if diff != 0: 40 | self.last_position = self.position 41 | return EncoderPostionEvent(diff) 42 | return None 43 | -------------------------------------------------------------------------------- /pico_macropad/hid.py: -------------------------------------------------------------------------------- 1 | import time 2 | from adafruit_hid.consumer_control import ConsumerControl 3 | from adafruit_hid.keyboard import Keyboard 4 | import usb_hid 5 | 6 | 7 | class HidInterface(): 8 | def send(self): 9 | pass 10 | 11 | def press(self): 12 | pass 13 | 14 | def release(self): 15 | pass 16 | 17 | 18 | class Key(HidInterface): 19 | _device = Keyboard(usb_hid.devices) 20 | 21 | def __init__(self, *keys: str): 22 | self.keys = keys 23 | 24 | def send(self): 25 | self._device.send(*self.keys) 26 | 27 | def press(self): 28 | self._device.press(*self.keys) 29 | 30 | def release(self): 31 | self._device.release(*self.keys) 32 | 33 | 34 | class ConsumerCtrl(Key): 35 | _device = ConsumerControl(usb_hid.devices) 36 | 37 | def __init__(self, key: str): 38 | super().__init__(key) 39 | 40 | def release(self): 41 | self._device.release() 42 | 43 | 44 | class Lambda(HidInterface): 45 | def __init__(self, callable): 46 | self.callable = callable 47 | 48 | def press(self): 49 | pass 50 | 51 | def release(self): 52 | self.send() 53 | 54 | def send(self): 55 | self.callable() 56 | 57 | 58 | class Multifunctional(HidInterface): 59 | long_press_duration = 0.5 60 | 61 | def __init__(self, short: HidInterface, long: HidInterface = None): 62 | self.tap = short 63 | self.long = long 64 | 65 | def press(self): 66 | self._pressed = time.monotonic() 67 | 68 | def release(self): 69 | released = time.monotonic() 70 | if (self.long and self.long_press_duration < released - self._pressed): 71 | self.long.send() 72 | else: 73 | self.tap.send() 74 | 75 | def send(self): 76 | raise Exception("Not implemented") 77 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import board 2 | import keypad 3 | from adafruit_hid.consumer_control_code import ConsumerControlCode 4 | from adafruit_hid.keycode import Keycode 5 | from pico_macropad.encoder import Encoder 6 | from pico_macropad.hid import HidInterface, Key, Lambda, ConsumerCtrl, Multifunctional 7 | import supervisor 8 | 9 | 10 | def main(): 11 | # Multifunctional.long_press_duration = 1 # change long press duration 12 | # create keys to send commands to the computer 13 | keys: list[HidInterface] = [ 14 | # row 1 15 | ConsumerCtrl(ConsumerControlCode.SCAN_PREVIOUS_TRACK), 16 | ConsumerCtrl(ConsumerControlCode.SCAN_NEXT_TRACK), 17 | Multifunctional(short=ConsumerCtrl(ConsumerControlCode.PLAY_PAUSE), 18 | long=Key(Keycode.CONTROL, Keycode.ALT, Keycode.S)), 19 | None, 20 | # row 2 21 | Multifunctional(short=Key(Keycode.CONTROL, Keycode.ALT, Keycode.F1), 22 | long=Key(Keycode.CONTROL, Keycode.ALT, Keycode.F2)), 23 | Key(Keycode.CONTROL, Keycode.ALT, Keycode.A), 24 | Multifunctional(short=Key(Keycode.WINDOWS, Keycode.SHIFT, Keycode.A), # PowerToys - mute microphone 25 | long=Key(Keycode.WINDOWS, Keycode.SHIFT, Keycode.O)), # PowerToys - turn off cam 26 | Key(Keycode.WINDOWS, Keycode.E), 27 | # row 3 28 | Key(Keycode.CONTROL, Keycode.ALT, Keycode.V), 29 | Key(Keycode.CONTROL, Keycode.ALT, Keycode.T), 30 | Multifunctional(short=Key(Keycode.F12), 31 | long=Lambda(lambda: supervisor.reload())), 32 | Multifunctional(short=Key(Keycode.WINDOWS, Keycode.D), 33 | long=Key(Keycode.WINDOWS, Keycode.CONTROL, Keycode.T)), # PowerToys - pin window 34 | ] 35 | encoder_cw_key = ConsumerCtrl(ConsumerControlCode.VOLUME_INCREMENT) 36 | encoder_ccw_key = ConsumerCtrl(ConsumerControlCode.VOLUME_DECREMENT) 37 | encoder_btn_key = ConsumerCtrl(ConsumerControlCode.MUTE) 38 | 39 | # create KeyMatrix to read key presses 40 | matrix = keypad.KeyMatrix( 41 | row_pins=(board.GP18, board.GP17, board.GP16), 42 | column_pins=(board.GP19, board.GP20, board.GP21, board.GP22) 43 | ) 44 | 45 | # create Encoder to read values 46 | encoder = Encoder(board.GP10, board.GP11, board.GP12, 2) 47 | 48 | while True: 49 | event = matrix.events.get() 50 | if event: 51 | key = keys[event.key_number] 52 | if event.pressed: 53 | key.press() 54 | else: 55 | key.release() 56 | 57 | event = encoder.getButtonEvent() 58 | if event: 59 | if event.pressed: 60 | encoder_btn_key.press() 61 | else: 62 | encoder_btn_key.release() 63 | 64 | event = encoder.getPositionEvent() 65 | if event: 66 | key = encoder_cw_key if event.difference > 0 else encoder_ccw_key 67 | for x in range(abs(event.difference)): 68 | key.send() 69 | 70 | 71 | if __name__ == "__main__": 72 | main() 73 | --------------------------------------------------------------------------------