Macro Handler (for Keybow2040 / Pico Keypad Base / etc)

The PMK library for these keypads is pretty nice, but I felt they were a bit limited in how they handled sending macros from the adafruit HID library, limiting each type of macro to a different layer. I felt it needed a bit more Oomph and flexibility.

The following module (in the next post) gives the ability to handle any macro type from any layer, and even chain multiple types together. It also exposes all the default HID interfaces and allows for adding custom handlers (and includes 3 by default). should work for anything using the adafruit_hid library, not just the keypads listed above.

I recommend stripping out the docstrings (text starting and ending with ‘’'), when saving it onto your device, save on space. They’re only there to describe how it works. better still would be to create an .mpy file from it.

The following code should be copied to a text file named “” and saved in the /lib folder on your device

# SPDX-FileCopyrightText: 2022 Nox Ferocia
# SPDX-License-Identifier: Unlicense

Unified Macro Handler (/lib/
Written in Adafruit Circuit Python for
SOFTWARE: adafruit_hid Library
Provides a composite of the default adafruit_hid interfacces,
macro handlers to simplfiy coding / macro writing, and an
extensible framework for additional custom macro handlers

import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.Mouse import Mouse

class HIDPool( object ):
	Composite Pool of all default adafruit_hid interfaces
		.key: adafruit_hid.keyboard.Keyboard
		.media: adafruit_hid.consumer_control.ConsumerControl
		.mouse: adafruit_hid.mouse.Mouse
		.text: adafruit_hid.keyboard_layout_us.KeyboardLayoutUS
	key = Keyboard( usb_hid.devices )
	media = ConsumerControl( usb_hid.devices )
	mouse = Mouse( usb_hid.devices )
	text = KeyboardLayoutUS( key )
# Workaround for bad assumption in adafruit_hid.keyboard.Keyboard.send
def parse_mod_plus( *macro_data ) -> None:
	Presses modifier keys, sends regular
	keys individually, releases modifer keys
	macro_data Format:
		modifer, ..., flag, regular, ...
		modifer: integer, key code, 1-5 modifiers, eg ALT
		flag: string, "MOD_PLUS", exactly
		regular: integer, key code, 1+ additional keys, eg F4
	_held_flag = True
	for _key_code in macro_data:
		if "MOD+" == _key_code:
			_held_flag = False
		if _held_flag: _key_code )
		else: _key_code )
			HIDPool.key.release( _key_code )
	for _key_code in reversed( macro_data ):
		if "MOD+" == _key_code:
			_held_flag = True
		if _held_flag:
			HIDPool.key.release( _key_code )

# Workaround for bad assumption in adafruit_hid.keyboard_layout_us.KeyboardLayoutUS.write
def parse_utf16( target_platform, utf16_code ) -> None:
	Sends keystrokes to enter a UTF16 character on the target platform
	"NIX": ChromeOS / Linux, "WIN":windows xp+, "MAC": MAcOS 8.5+
	Windows and MacOS both require enabling Unicode Hex entry for this to work
		target_platform: string, "NIX", "WIN", "MAC"
		utf16_code: string, 4 hexadecimal digits
	See Also:
	if "NIX" == target_platform: 0xE4, 0xE5, 0x18 )
		HIDPool.key.release( 0x18 )
	elif "WIN" == target_platform: 0xE6 ) 0x57 )
		HIDPool.key.release( 0x57 )
	elif "MAC" == target_platform: 0xE2 )
	for _hex_digit in utf16_code:
		# Use last index returned for safety with capitlized hex
		_hex_keycode = HIDPool.text.keycodes( _hex_digit )[-1] _hex_keycode )
		HIDPool.key.release( _hex_keycode )
	if "MAC" == target_platform:
		HIDPool.key.release( 0xE2 )
	elif "WIN" == target_platform:
		HIDPool.key.release( 0xE6 )
	elif "NIX" == target_platform:
		HIDPool.key.release( 0xE5, 0xE4 )

def parse_mouse_select( mouse_buttons, x_axis, y_axis, scroll ) -> None:
	Presses specified mouse buttons, moves/scrolls mouse, releases mouse buttons
		mouse_buttons: integer, sum of desired buttons, (1:left, 2:right, 4:middle)
		x_axis: integer, +right/-left, 0-127, horizontal axis movement
		y_axis: integer, +up/-down, 0-127, vertical axis movement
		scroll: integer, +up/-down, 0-127, scroll wheel movement
	''' mouse_buttons )
	HIDPool.mouse.move( x_axis, y_axis, scroll )
	HIDPool.mouse.release( mouse_buttons )

_macro_parsers = {
"MOD+": parse_mod_plus,
"UTF16": parse_utf16,
"MOUSE SELECT": parse_mouse_select,

class MacroHandler( HIDPool ):
	Composite aggregator class for sending HID macros
	# TODO: potentially move attributes here
	def __init__( self, parser_dictionary = _macro_parsers ) -> None:
		Adds dictionary of parser functions to the instance
		Creates aliases to aggregate HID communications
			parser_dictionary: dictionary, {"parser id": parser_function, ...}
		self.__internal_parsers = {
		"KEY": self.key.send,
		"MOUSE MOVE": self.mouse.move,
		"TEXT": self.text.write,
		self.__external_parsers = parser_dictionary
	def send_macro( self, macro_container ) -> None:
		Checks type of macro container and processes accordingly.
		macro_container Format: (either of)
			List: [("PARSER_ID", PARSER_MACRO), ...]
			macro: tuple, ( "PARSER_ID", PARSER_MACRO )
				"PARSER_ID": string, id of a parser
				PARSER_MACRO: varies, see related function
		For builtin parser IDs SEE ALSO:
			KEY: adafruit_hid.keyboard.Keyboard.send
			MEDIA: adafruit_hid.consumer_control.ConsumerControl.send
			MOUSE_CLICK: adafruit_hid.mouse.Mouse.send
			MOUSE_MOVE: adafruit_hid.mouse.Mouse.move
			TEXT: adafruit_hid.keyboard_layout_us.KeyboardLayoutUS.write
		if tuple == type( macro_container ):
			macro_container = [macro_container]
		for _macro in macro_container:
			if _macro[ self.MACRO_TYPE ] in self.__internal_parsers:
				self.__internal_parsers[ _macro[ self.MACRO_TYPE ] ]( *_macro[ self.MACRO_DATA: ] )
			elif _macro[ self.MACRO_TYPE ] in self.__external_parsers:
				self.__external_parsers[ _macro[ self.MACRO_TYPE ] ]( *_macro[ self.MACRO_DATA: ] )
	def get_handler_list( self ) -> list:
			Dictionary, {Parser id, function} pairs
		return sorted( self.__internal_parsers ) + sorted( self.__external_parsers.copy() )
	def add_handler( self, new_id, new_function, override = False ) -> bool:
		Adds a new external parser to the dictionary
			new_id: string, id for parser function
			new_function: function name, without ()
			override: boolean, optional, True to replace preexisting id
				"KEY", "MEDIA", "MOUSE_CLICK", "MOUSE_MOVE", & "TEXT" cannot be added/overridden
			boolean, True if successful, False if denied
		if new_id in self.__internal_parsers or (new_id in self.__external_parsers and not override):
			return False
		self.__external_parsers[new_id] = new_function
		return True
	def del_handler( self, remove_id ) -> bool:
		Removes an external parser from the dictionary
			remove_id: string, id of a parser in the dictionary
			boolean, True if removed, False otherwise
				"KEY", "MEDIA", "MOUSE_CLICK", "MOUSE_MOVE", & "TEXT" cannot be removed
		if remove_id not in self.__external_parsers:
			return False
		self.__external_parsers.pop( remove_id )
		return True


Where you would normally write:

import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.Mouse import Mouse
keyboard = Keyboard( usb_hid.devices )
layout = KeyboardLayoutUS( keyboard )
consumer_control = ConsumerControl( usb_hid.devices )
mouse = Mouse( usb_hid.devices )

you can now just write:

from macro_handler import MacroHandler
macro_comm = MacroHandler()

The basic hid functions can be accessed from

  • macro_comm.key → (all the keyboard methods)
  • → (all the consumer control methods)
  • macro_comm.mouse → (all the mouse methods)
  • macro_comm.text → (all the layout methods)

all of which can be used to write your own custom functions

The handler is the star of the show though. It lets you write macros containers like

(macro_type, macro_data)
# or
[(first_macro_type, first_macro_data), (second_macro_type, second_macro_data), ...]

and send them with

macro_comm.send_macro( macro_container )

and here’s an example you can try from Mu, Thonny, or your REPL of choice

from adafruit_hid.keycode import Keycode as key_code # to make key codes easier to look up
from macro_handler import MacroHandler
macro_comm = MacroHandler()
macro_comm.send_macro( [("TEXT", "o/ There is one thing I must say to you"), ("KEY", key_code.ENTER), ("TEXT", "As you sail across the sea o/")] )