Super Cheap Macro Keyboard

by Guitarman9119 in Circuits > Raspberry Pi

288 Views, 2 Favorites, 0 Comments

Super Cheap Macro Keyboard

I made the Cheapest Macro Keyboard

Welcome to this instructable on creating a really super cheap macro keyboard that does not require any tools or equipment based on the Raspberry Pi Pico and Circuit Python firmware! By the end of this instructable, you’ll have a fully functional macro keyboard that you can customize to your heart’s content. So let’s get started!

What Is A Macro Keyboard?

Macro keyboards are an excellent way to streamline your workflow and increase productivity. With a macro keyboard, you can automate repetitive tasks, execute complex commands with a single key press, and customize your keyboard layout to your needs.

Supplies

comp.png

Components

1 - Raspberry Pi Pico

1 - 4x4 Keypad

1 - SSD1306 1.3" OLED I2C

2 - Rotary encoder

Wires Various size

1 - Box for enclosure

Most of these components you can easily buy from AliExpress or electronics stores near you.

Schematic Diagram

supercheapmacro.png

The schematic diagram is shown above. The design is straightforward as each button was connected to a GPIO pin to the Raspberry Pi Pico. After making all the connections ensure to setup your Raspberry Pi Pico with CircuitPython firmware.

Building the Macro Keyboard

macr.PNG

This step is up to you on how you want to make the enclosure for you Macro Keyboard. The route I take was very simple I just attached everything to a random box I had laying around to avoid using any machines like 3D printer or Laser cutter. If you do make another version please share it because I would like to see it.

Setup Pico - Circuit Python

download.png

CircuitPython is a variant of the Python programming language designed for microcontrollers, specifically those based on the ARM Cortex-M family of processors. It allows developers to write Python code that can interact with hardware components and sensors, making it a popular choice for DIY electronics projects.

One advantage of using CircuitPython for building a macro keyboard is that it includes the HID (Human Interface Device) library, which allows microcontrollers to act as USB input devices such as keyboards, mice, and gamepads. This means that with CircuitPython, you can program your microcontroller to act like a keyboard and send keystrokes to your computer when a button is pressed on your macro keyboard.

Another advantage of CircuitPython is its ease of use and rapid prototyping capabilities. Since CircuitPython is an interpreted language, developers can write and test code on their computer before uploading it to the microcontroller. Additionally, CircuitPython comes with a large number of built-in libraries and modules that simplify programming for common hardware components, such as OLED displays, sensors, and LED strips. This can save a lot of time and effort compared to writing low-level code in C or assembly language.

Watch the following video on how to setup CircuitPython on your Pico.

HID Library

HID.PNG

In order to get the Raspberry Pi Pico to work as an macro keyboard we need to install a Human Interface Device Library from Adafruit Circuit Python library.

You need to create a folder on your Raspberry Pi Pico named “adafruit_hid” and upload the following files in the folder. You can download the files on my GitHub repository. It is also available on Circuit Python website but in order to keep version control for this specific tutorial along with the video I recommend you download it from my GitHub repository.

Code Explanation

You can download all the code here: Code

Unzip the code and copy and paste it on your Raspberry Pi Pico mass storage device, and overwrite all the files. The Code file contains all the libraries you need for this project.

Here is an overview of the code.py file which is the code that runs on the Pico is powered on:

1. Importing Libraries and Modules:

import board, busio, displayio, os, terminalio, keypad
import adafruit_displayio_ssd1306
from adafruit_display_text import label
import usb_hid
import digitalio
import time
import rotaryio

from adafruit_hid.keycode import Keycode
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode

from blender_mode import handle_keypress as blender_mode_handle_keypress
from windows_mody import handle_keypress as windows_mode_handle_keypress
from premier_mode import handle_keypress as premier_mode_handle_keypress
from aftereffects_mode import handle_keypress as aftereffects_mode_handle_keypress
from fusion360_mode import handle_keypress as fusion360_mode_handle_keypress

This section imports various libraries and modules necessary for handling hardware, display, input, USB HID, and specific functions related to different software modes (blender_mode, windows_mode, etc.).

2. Setting up Consumer Control and Keyboard:

cc = ConsumerControl(usb_hid.devices)
keyboard = Keyboard(usb_hid.devices)
write_text = KeyboardLayoutUS(keyboard)

Instances of ConsumerControl and Keyboard classes are created to handle consumer control and keyboard functionalities.

3. Initializing Display:

displayio.release_displays()
sda, scl = board.GP16, board.GP17  
i2c = busio.I2C(scl, sda)
display_bus = displayio.I2CDisplay(i2c, device_address=0x3C)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64)

The OLED display is initialized using the adafruit_displayio_ssd1306 library.

4. Creating Display Elements:


splash = displayio.Group()

# Creating bitmaps and palettes for display elements
color_bitmap = displayio.Bitmap(128, 64, 1)
color_palette = displayio.Palette(1)
inner_bitmap = displayio.Bitmap(118, 54, 1)
inner_palette = displayio.Palette(1)

# Creating TileGrids for background and inner rectangle
bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0)
inner_sprite = displayio.TileGrid(inner_bitmap, pixel_shader=inner_palette, x=5, y=5)

# Creating labels for text
text_area = label.Label(terminalio.FONT, text="NerdCave!", color=0xFFFF00, x=40, y=18)
text_area2 = label.Label(terminalio.FONT, text="Super Cheap", color=0xFFFF00, x=28, y=31)
text_area3 = label.Label(terminalio.FONT, text="Macrokeyboard", color=0xFFFF00, x=26, y=44)

# Appending display elements to the splash group
splash.append(bg_sprite)
splash.append(inner_sprite)
splash.append(text_area)
splash.append(text_area2)
splash.append(text_area3)

# Showing the splash group on the display
display.show(splash)

Display elements such as background, inner rectangle, and text labels are created and added to the display using the displayio library.

5. Setting Up Keypad:

km = keypad.KeyMatrix(
    row_pins=(board.GP2, board.GP3, board.GP4, board.GP5),
    column_pins=(board.GP6, board.GP7, board.GP8, board.GP9),
)

A 4x4 keypad is set up using the keypad.KeyMatrix class.

6. Setting Up Rotary Encoders:

DT_Pin1 = digitalio.DigitalInOut(board.GP18)
CLK_Pin1 = digitalio.DigitalInOut(board.GP19)
SW1 = digitalio.DigitalInOut(board.GP14)
DT_Pin2 = digitalio.DigitalInOut(board.GP20)
CLK_Pin2 = digitalio.DigitalInOut(board.GP21)
SW2 = digitalio.DigitalInOut(board.GP15)
# (Initialization code for other pins...)
previousValue = 1
previousValue2 = 1

Two rotary encoders with associated pins and push-button switches are initialized.

7. Rotary Encoder Functions:

def rotary_changed_left():
    global previousValue
    if previousValue != CLK_Pin1.value:
        if CLK_Pin1.value == 0:
            if DT_Pin1.value == 0:
                return False
            else:
                return True
        previousValue = CLK_Pin1.value
    return None


def rotary_changed_right():
    global previousValue2
    if previousValue2 != CLK_Pin2.value:
        if CLK_Pin2.value == 0:
            if DT_Pin2.value == 0:
                return False
            else:
                return True
        previousValue2 = CLK_Pin2.value
    return None

Functions to handle changes in rotary encoder values are defined.

8. Mode Names and Default Mode:

mode_names = {1: 'Blender', 2: 'Windows', 3: 'Premier Pro', 4: 'After Effects', 5: 'Fusion360'}
mode = 0

A dictionary (mode_names) associates mode numbers with their names, and the default mode is set to 0.

9. Updating Macro Label Function:

def update_macro_label(macro_name):
    macro_label = label.Label(terminalio.FONT, text=macro_name, color=0xFFFF00, x=0, y=55)
    splash.append(macro_label)
    display.refresh()
    time.sleep(3)
    splash.remove(macro_label)
    display.refresh()

A function to update the OLED display with the current macro mode label.

10. Main Loop:

while True:
    event = km.events.get()
    if event:
        if event and event.pressed and event.key_number == 15:
            # (Code for mode increment and display update...)
        else:
            print(event)


        if mode == 0:
            time.sleep(0.01)


    if mode == 1:
        blender_mode_handle_keypress(event, cc, write_text, keyboard, SW1, SW2, rotary_changed_left, rotary_changed_right, splash, display)
    # (Code for handling other modes...)
    
    time.sleep(0.001)
  • The main loop continuously checks for keypad events and rotary encoder changes. Depending on the current mode, specific functions from custom modules are called to handle keypress events.

For each application we have a different script, here we will look at the blender script:

1. Importing Libraries and Modules:

import time
import board, busio, displayio, os, terminalio
import digitalio
from adafruit_hid.keycode import Keycode
from adafruit_hid.consumer_control_code import ConsumerControlCode
from adafruit_display_text import label
import adafruit_displayio_ssd1306

This section imports various libraries and modules necessary for handling time, hardware, display, input, and USB HID functionalities.

2. Function: update_screen:

def update_screen(splash, macro_name, display):
    center_x = (118 - len(macro_name) * 6) // 2 + 5
    macro_label = label.Label(terminalio.FONT, text=macro_name, color=0xFFFF00, x=center_x, y=50)
    splash.append(macro_label)
    display.refresh()
    time.sleep(1)
    splash.remove(macro_label)
    display.refresh()
  • This function updates the OLED display with a macro name for a brief period (3 seconds).
  • The splash is the display group, macro_name is the text to be displayed, and display is the OLED display

3. Function: handle_keypress:

def handle_keypress(event, cc, write_text, keyboard, SW1, SW2, rotary_changed_left, rotary_changed_right, splash, display):
  macro_names = {
    0: "Play / Pause",
    1: "Play/Pause",
    2: "Open Chrome",
    3: "Volume Up",
    4: "Grab",
    # Add more macro names and their corresponding keys as needed
  }
   
  if event and event.pressed and event.key_number == 0:
    cc.send(ConsumerControlCode.PLAY_PAUSE)
    time.sleep(0.1)
    update_screen(splash, macro_names[0], display)
     
  # (Similar blocks for other key numbers...)
   
  # Handling rotary encoder 1
  if rotary_changed_left() == True:
    keyboard.send(Keycode.RIGHT_ARROW)
     
  elif rotary_changed_left() == False:
    keyboard.send(Keycode.LEFT_ARROW)
    
  # Handling rotary encoder 2
  if rotary_changed_right() == True:
    cc.send(ConsumerControlCode.VOLUME_INCREMENT)
    time.sleep(0.01)
     
  elif rotary_changed_right() == False:
    cc.send(ConsumerControlCode.VOLUME_DECREMENT)
    time.sleep(0.01)
     
  # Handling button press for rotary encoder 1
  if SW1.value == 0:
    keyboard.send(Keycode.RIGHT_ARROW)
    print("Rotary 1 - Button pressed")
    time.sleep(0.2)
     
  # Handling button press for rotary encoder 2
  if SW2.value == 0:
    print("Rotary 2 - Button pressed")
    time.sleep(0.2)
  • This function handles keypress events based on the event's key number.
  • It includes specific actions for different key numbers (0 to 4) and executes corresponding functions.
  • It also handles the rotation and button press events for two rotary encoders (left and right).

This code defines functions for handling keypress events on a macro keyboard with OLED display and rotary encoders. The update_screen function updates the display with a specific macro name, and the handle_keypress function defines actions for various keys and rotary encoder events. The code is structured for easy customization and addition of more macros.

This breakdown should help you understand the code structure and functionality. If you have specific questions about any part of the code, feel free to ask!

Conclusion

I hope you found this tutorial helpful. I am planning on making another PCB version macro keyboard in the future. If that is something you will be interested in then consider giving me a follow here on instructables.