Pico RGB keypad with micropython HID

The Pimoroni examples for using the Pico RGB keypad as a HID device only shows using Circuit Python and its Micropython demo just shows using the keypad as a colourful push button device.

This is probably because the micropython HID module for the Pico was not in created at the time the Pico keypad was introduced, though the mp HID module has been available for quite some time now. Theres nothing wrong with using Circuit Python of course, but I wanted to see if I could use the mp HID

Theres a dearth of examples of using mp HID so I show a short example of using this module on a Pico. The mp HID module has to be installed as its not part of the standard mp installation. I installed it on a Pico2W thus making it easy to install with the mp ‘mip’ module that is part of the standard mp installation for ‘W’ modules. An example of the install process is

  1. connect the Pico W to wifi.

  2. at the REPL:

    import mip

    mip.install(“usb-device-keyboard”)

Then the usb library will be installed into the PicoW’s lib folder as .mpy files. Its probably helpful to read the .py files and the keyboard.py file can be found at:

https://github.com/micropython/micropython-lib/blob/master/micropython/usb/usb-device-keyboard/usb/device/keyboard.py

Within the keyboard.py file a ‘class KeyCode:’ can be found where class variables are assigned to various keycode values.

The example below makes the Pico into a HID device and fires off a couple of key codes to whatever the Pico’s usb is attached to when its powered up. (hopefully something like a terminal session on a computer :) )

Do note that if the code is run with the likes of a Thonny IDE, when the program is run, thus turning the Pico into a HID device, the IDE link to the pico is severed. However the program can be coded up and tested in Thonny by commenting out the

usb.device.get().init(kb, builtin_driver=True)

code line and perhaps testing the keycode being delivered with print statements. To then run the code, remove power from the Pico and then restore it again and it will then be a HID device. Clicking the Thonny Stop/Restart button a couple of times will restore the IDE link to the Pico again.

Also note a small delay in the code to allow the Pico, now as a usb HID device, to establish its usb connection to its host before sending any keycodes, and that the keycodes are sent to the kp.send_keys(key) function as a list or tuple

Once the use of the mp HID is established then of course its easy to make the Pico RGB keypad keys send the desired keycodes as a HID device. My Pico keypad is programmed to do a double duty of a keyboard HID device, but with some keys sending mqtt messages instead. Now I’ve just got to remember which keys do what, but they are coloured to serve as a reminder and do flash in nice colours when pressed :)

import usb.device
from usb.device.keyboard import KeyboardInterface, KeyCode
import time

kb = KeyboardInterface()
# the following will make the pico's usb port into a Keyboard device and
# Thonny will loose its link to the pico 
# - comment out when creating program and test with print(key)
usb.device.get().init(kb, builtin_driver=True)

def sendkey(key):
    #print(key)
    kb.send_keys(key)
    kb.send_keys([])  

# Examples of KeyCode charaters which must be passed to 
# kb.send_kyes(key) as a list or tuple.
key_1 = [KeyCode.N1]
key_star = [KeyCode.KP_MULTIPLY]
key_a = [KeyCode.A]
key_A = [KeyCode.LEFT_SHIFT, KeyCode.A]
# to get '#' character for mac uk keyboard
key_hash = [KeyCode.LEFT_ALT, KeyCode.N3]


# Send KeyCodes
# without a small sleep the following keycodes will not get sent
time.sleep(1) 

sendkey(key_1)
sendkey(key_star)
sendkey(key_a)
sendkey(key_A)
sendkey(key_hash)



2 Likes

I’ll be tagging along on this one. I have a Pico RGB Keypad Base on my desk coded up in Circuit Python. Everything else I have is Python or Micro Python. I also have a spare keypad I can flash with Micro Python and tinker with the code you posted. =)

@alphanumeric The same for me, I had my keypad running Circuit Python HID code but when I wanted to change the code I had a go at getting mp HID to work as its my preferred environment.

You may be interested in a slightly more advanced example. I had a Pimoroni example code for the keypad (rgbkeypad.py) that creates a key object for each of the keys and puts them in a list of keys for manipulation. (I think it was a Pimoroni example, but I cant see it on their examples when I just looked). Anyway I amended this example to include the storing an assigned key value and renamed the file to bg_rgbkeypad.py. I show this file below and also an example that makes use of it.
The sending of bunch of HID keys values as a long ‘macro’ list will not work as it trips up when sending more than 3 keycode values, so the example hidkey() function in the example sends any list of keycodes greater than 3 values as individual keycodes. (if you see what I mean). The example shows this for the keypad[3] which sends ‘ls -a’ to list the files in a directory (on my mac)

As before comment out the code line ‘usb.device.get().init(kb, builtin_driver=True)’ to enable the example code to be tested in Thonny with print statements, but restore the line to run it as a HID program outside of Thonny.

bg_rgbkeypad.py:


import machine

PIN_SDA = 4
PIN_SCL = 5
PIN_CS = 17
PIN_SCK = 18
PIN_MOSI = 19


class RGBKeypad():
  
    KEYPAD_ADDRESS = 32

    # ------------------------------------------------------------------
    class MPDevice(): 
        def __init__(self):
            """
            Internal class used to communicate with the keypad device when
            using MicroPython.
            """
            # setup i2c
            self._i2c = machine.I2C(
                0, 
                scl=machine.Pin(PIN_SCL), 
                sda=machine.Pin(PIN_SDA), 
                freq=400000
                )

            # setup spi
            self._spi = machine.SPI(
                0, 
                baudrate=4*1024*1024, 
                sck=machine.Pin(PIN_SCK), 
                mosi=machine.Pin(PIN_MOSI)
                )
            
            # setup cs
            self._cs = machine.Pin(
                PIN_CS, 
                machine.Pin.OUT
                )
            self._cs.high()
        
        def read_keys(self):
            self._i2c.writeto(RGBKeypad.KEYPAD_ADDRESS, bytearray(1), True)
            data = self._i2c.readfrom(RGBKeypad.KEYPAD_ADDRESS, 2, False)
            return data

        def write_leds(self, led_data):
            self._cs.low()
            self._spi.write(led_data)
            self._cs.high()
            
    # ------------------------------------------------------------------
    class RGBKey():

        def __init__(self, keypad, x, y, red, green, blue, brightness,assigned_key=None):
            """
            Represents a single key of the RGB Keypad. 

            Returned when using RGBKeypad[x,y] or RGBKeypad.get_key(x,y)::

                keypad = RGBKeypad()
                key = keypad[0, 0]

            """
            self._keypad = keypad
            self._x = x
            self._y = y
            self._red = red
            self._green = green
            self._blue = blue
            self._brightness = brightness
            self._assigned_key = assigned_key
            
        @property
        def x(self):
            """
            Returns the x position of the key.
            """
            return self._x

        @property
        def y(self):
            """
            Returns the y position of the key.
            """
            return self._y

        @property
        def brightness(self):
            """
            Sets or returns the brightness of the key. 

            A value between 0 and 1 where 0 is off and 1 is full brightness.
            """
            return self._brightness

        @brightness.setter
        def brightness(self, value):
            value = max(min(1, value), 0)
            self._brightness = value
            self._update()

        @property
        def assigned_key(self):
            return self._assigned_key

        @assigned_key.setter
        def assigned_key(self,value):
            self._assigned_key = value

        @property
        def red(self):
            """
            Sets or returns the red color of the key.

            A value between 0 and 255.
            """
            return self._red
        
        @red.setter
        def red(self, value):
            value = max(min(255, value), 0)
            self._red = value
            self._update()

        @property
        def green(self):
            """
            Sets or returns the green color of the key.

            A value between 0 and 255.
            """
            return self._green
        
        @green.setter
        def green(self, value):
            value = max(min(255, value), 0)
            self._green = value
            self._update()

        @property
        def blue(self):
            """
            Sets or returns the blue color of the key.

            A value between 0 and 255.
            """
            return self._blue
        
        @blue.setter
        def blue(self, value):
            value = max(min(255, value), 0)
            self._blue = value
            self._update()

        @property
        def color(self):
            """
            Sets or returns the color of the key.

            A tuple of (red, green, blue) values between 0 and 255.
            """
            return (self.red, self.green, self.blue)

        @color.setter
        def color(self, value):

            auto_update_value = self._keypad.auto_update

            self.red = value[0]
            self.green = value[1]
            self.blue = value[2]

            self.auto_update = self._keypad.auto_update

            self._update()

        def is_pressed(self):
            """
            Returns True if the key is pressed when called.
            """
            return self._keypad.get_keys_pressed()[
                (4 * self._y) + (self._x % 4)
            ]

        def clear(self):
            """
            Clears the key color. 

            Clear is the same as setting the color to black (0, 0, 0).
            """
            self.color = (0, 0, 0)

        def _update(self):
            if self._keypad.auto_update:
                self._keypad.update()

    # ------------------------------------------------------------------

    def __init__(self, color=(0,0,0), brightness=0.5, auto_update=True):
        """
        Represents a pimoroni RGB Keypad device connected to a Raspberry Pi
        pico running either MicroPython or CircuitPython.

        ::

            rgbkeypad = RGBKeyPad()

        A single key can be obtained using its x, y coordinate ::

            key = rgbkeypad[0, 0]

        :param tuple color:
            The initial color for all the keys. 

            A tuple of (red, green, blue) values between 0 and 255.

            The default is (0, 0, 0), black or off.

        :param int brightness:
            The initial brightness of the keys.

            A value between 0 and 1 where 0 is off and 1 is full brightness.

            The default is 0.5.

        :param bool auto_update:
            When True, the key pad will be automatically updated
            when the color or brightness of a key is changed.

            If False, the update() method will need to be called
            before any changes are reflected on the keypad.

            The default is True
        """
        # create the device
        self._device = RGBKeypad.MPDevice()
 
        # setup all the keys before setting aut o update
        self.auto_update = False

        # setup keys
        self._keys = []
        for y in range(4):
            for x in range(4):
                self._keys.append(
                    RGBKeypad.RGBKey(self, x, y, color[0], color[1], color[2], brightness)
                    )
        
        self.update()
        
        self.auto_update = auto_update
        self._color = color
        self._brightness = brightness

    def clear(self):
        """
        Clears all the keys color. 

        Clear is the same as setting the color to black (0, 0, 0).
        """
        self.color = (0, 0, 0)

    @property
    def color(self):
        """
        Sets the color of all the keys as a tuple of (red, green, blue) values
        between 0 and 255.

        Note - color will return the "default" or "initial" color of the keys. 
        If an individual key's color has been change this wont be represented.
        """
        return self._color
    
    @color.setter
    def color(self, value):
        auto_update_value = self.auto_update

        self.auto_update = False
        for key in self._keys:
            key.color = (value[0], value[1], value[2])
        self.update()

        self.auto_update = auto_update_value
    
    @property
    def brightness(self):
        """
        Sets the brightness of all the keys as a value between 0 and 1 where 
        0 is off and 1 is full brightness.

        Note - brightness will return the "default" or "initial" brightness 
        of the keys. If an individual key's brightness has been change this 
        wont be represented.
        """
        return self._brightness

    @brightness.setter
    def brightness(self, value):
        auto_update_value = self.auto_update

        self.auto_update = False
        for key in self._keys:
            key.brightness = value
        self.update()

        self.auto_update = auto_update_value
    
    @property
    def keys(self):
        """
        Returns a list of all the keys as RGBKey objects.
        """
        return self._keys
    
    def get_keys_pressed(self):
        """
        Returns a list of 16 booleans represent the current pressed
        state of all the keys.
        """
        data = self._device.read_keys()
        button_data = int.from_bytes(data, "little")
        
        # button states
        button_states = []
        for button in range(16):
            button_states.append(0 == (button_data & (1<<button)))

        return button_states

    def get_key(self, x, y):
        """
        Returns a key as an RGBKey object.

        get_key(x, y) is the equivalent of RGBKeypad[x, y].

        :param int x:
            The x position of the key.

        :param int y:
            The y position of the key.
        """
        return self._keys[(4 * y) + (x % 4)]

    def update(self):
        """
        Updates the RGB Keypad with the current color and brightness.

        Is only required to be called when auto_update = False.
        """
        led_data = bytearray((16*4) + 8)
        data_pos = 4

        for key in self._keys:
            led_data[data_pos] = int(255 - 31 + (31 * key.brightness))
            led_data[data_pos + 1] = key.blue
            led_data[data_pos + 2] = key.green
            led_data[data_pos + 3] = key.red

            data_pos += 4

        self._device.write_leds(led_data)

    def __getitem__(self, index):
        return self.get_key(index[0], index[1])


test.py

from bg_rgbkeypad import RGBKeypad
import usb.device
from usb.device.keyboard import KeyboardInterface, KeyCode, LEDCode
import time

kb = KeyboardInterface()
usb.device.get().init(kb, builtin_driver=True)

keypad = RGBKeypad()

def sendkey(key):
    if key:
        if key[0] == 'msg':
            print('mqtt message to publish:',key[1], key[2])
        else:
            hidkey(key)

def hidkey(key):
    if len(key) < 4:
        print(key)
        kb.send_keys(key)
        kb.send_keys([])
    else:
        for num in range(0, len(key),1):
            print(key[num])
            kb.send_keys([key[num]])
            kb.send_keys([])

def key_colour_end(key):
    key.color=(0,0,0)

# -------------------------------------------
# assign keycodes to keypad keys

# keycode (for mac) to list all files, including hidden, in the current dir.
keypad.keys[3].assigned_key = [KeyCode.L, KeyCode.S, KeyCode.SPACE,   KeyCode.MINUS, KeyCode.A, KeyCode.ENTER]
keypad.keys[3].color = (0,0,200)

# keypad key 7 to send an mqtt message
keypad.keys[7].assigned_key = ['msg', 'request/wf/today', '']
keypad.keys[7].color = (100,200,100)

# keypad keys set to send HID keycodes
keypad.keys[12].assigned_key = [KeyCode.A]
keypad.keys[12].color = (0,100,0)
keypad.keys[13].assigned_key = [KeyCode.LEFT_SHIFT, KeyCode.A]
keypad.keys[13].color = (0,100,0)
keypad.keys[14].assigned_key = [KeyCode.LEFT_ALT, KeyCode.N3]
keypad.keys[14].color = (0,100,0)
keypad.keys[15].assigned_key = [-0x04,32]
keypad.keys[15].color = (0,100,0)


# continually loop to check what keypad key is pressed
try:
    while True:
        for key in keypad.keys:
            if key.is_pressed():
                assigned_color=key.color
                key.color=(225,0,0)
                print("key", key.x, key.y, "pressed / value:", key.assigned_key)
                sendkey(key.assigned_key)
                time.sleep(0.25)
                key.color=assigned_color
        time.sleep(0.01)

except KeyboardInterrupt:
    print('Ctl C')
    keypad = RGBKeypad() # to put all key colours back to default (off)
    

So just a simple example. I’ve been looking in my electronic bits boxes for a nice little i2c screen (no luck) to hook up and to print a 3d case for the keyboard and small screen. I will eventually get around to producing async code so that this keypad arrangement can both send and receive mqtt messages, display info on a small screen, and act as a HID keyboard. Good luck with testing out your mp HID.

1 Like