Trying to combine the HID Keys Advanced with Rpico OBS

Good Morning All,

So I’ve started messing around with my Keybow finally and I’ve gotten a bit stuck on a particular issue. I’m trying to replace a layer on the HID Keys Advance with the RPICO_RGB_Keypad_OBS.

I want to replace Layer 2 with elements from the RPICO_RGD_Keypad, I don’t mind sitting and learning how to do it but I have not a clue how to get it to work properly or even get it to fit correctly. My knowledge and time with CircuitPython is limited at best. It took me hours to figure out how to comment/uncomment. D:

Those two examples use very different styles of execution (direct look up vs assigned decorators), so aren’t directly compatible, and would require heavy rewrites to add the functions of the other. Assuming the goal is to add layer functionality to the OBS toggle/lighting it is possible, but it’s not going to be a quick and easy thing. It is probably easier to modify the OBS side.

a sketch of the shallowest required changes would be:

  • add a setup for the USER_KEY so you can detect keypress on it
  • duplicate “config” in a new variable for your additional layer, change both variable names
  • insert all duplicate “config” variables into a new dictionary and add a selector variable, and a config = new_dictionary[ selector_variable] assignment
  • optional: add a mirror dictionary of toggle states for each layer that consists of {selector: state} with the default being off
  • turn the “add runtime data” into a function then call the function
  • in the “main” section, add detection for the USER_KEY, and on detection
    • either check for toggled keys and untoggle them, or save the toggle state for previous layer
      • not doing this will give you annoying bugs related to toggled keys
    • selector change can be complex or as a simple as “selector = not selector”
    • reassign the layer with "config = new_dictionary[ selector ]
    • call the “add runtime data” function
    • load the new layer toggle states (if you are saving the previous ones)

*: USER_KEY is the side button right of usb-c port on the keybow2040

Why modify the OBS file and not the “advanced” example? The toggle functions of the OBS example, as written, rely decorators, and since you cannot turn off decorators once assigned, you’d have to add toggle and tracking logic to the “advanced” example along with multiple key actions. adding layer functions to the OBS side lets you do less code writing

1 Like

Ahh! Thanks for all of this and I actually understand it more now that you’ve explained it! Thanks for you help again!!!

I figured I’d just use the OBS one for now but I’ll do some practice with the OBS file as mentioned.

I may (ok definitely) have been thinking a bit to deeply on this, and I realized that I was technically right about not being able to disable the decorations… but effectively wrong because the code inside the decorators can be disabled by checking the layer variable

four tweaks in the OBS code.py file

config = [
    {}, # 0 <-- this line changed
    {"hue": hue["yellow"] , "group": "scene", "keycodes_on": [Keycode.F14],                  "keycodes_off": None                      }, # 1
...
for i in range(16): # there are three lines that have this text
for i in range(1, 16) # replace them all with this text (keep the same indent)
...
def press_kcs(kcs): # original file line
    If( current_layer == 2 ): # add new line below original
        print(f'keycode press {kcs} {KC_LIVE}') # Indent the rest of the block 1 level (block end at next blank line)
...
def release_kcs(kcs):  # original file line
    If( current_layer == 2 ): # add new line below original
        print(f'keycode release {kcs} {KC_LIVE}') # Indent the rest of the block 1 level (block end at next blank line)

then copy everything above

# Main loop
while True:

into the “advanced” example, above

while True:
    # Always remember to call keybow.update()!
    keybow.update()

and make sure to move any lines that start with “import” up to the top with the other “import” lines (and remove any duplicates)

copy everything from the OBS file below

    keybow.update()

to the following position in the “advanced” example

                elif current_layer == 2: # Original line, no changes
                    # Replace anything between the original lines with what you just copied, and add 3 indent levels to everthing pasted
                elif current_layer == 3: # Original line, no changes

and one tweak in the advanced file, replace all of this…

layer_2 =     {7: "pack ",
               11: "my ",
               15: "box ",
               6: "with ",
               10: "five ",
               14: "dozen ",
               5: "liquor ",
               9: "jugs "}

with this

layer_2 =     {} #use config for the OBS layer

and that hack should work, and even preserve the lighting and toggles of the “OBS” file

the only downside is that you’ll lose the 0 key from the “OBS” file (you can rearrange them to keep the 15 you want), because the “advanced” example needs it to switch layers, unless as mentioned in previous post you set up the user key for layer switching, which will require several more edits to the “advanced” example, but does open up the possibility to use all 16 main keys for macros

ETA:
yes, I nerd-sniped myself, also, here’s links to the repositories being used for anyone else that comes along and is curious
PMK/hid-keys-advanced example
rpico_rgb_keypad_obs

Sorry for taking long to respond, things got a bit hectic on my end.

Okay just so I’m making sure, as mentioned my knowledge of the coding is fairly new.

I’m putting the four tweaks here:

then I’m copying everything above the # Main Loop from Line 264 all the way back up?

I don’t mind losing the 0 key really, just wanted to be able to multi-use the device when I’m not streaming.

I edited my previous post to be a bit more explicit on the changes

1 Like

Alrighty, I think I follow the directions. Thank you for making them much clearer to understand.

In any case I’ve pasted the code here for a correction if it doesn’t look like you said.

https://github.com/firstfridayfunk/HIDpractice

sorry for the long delay, this is what the changed file should look like with ## comments to show changes.
WARNING!!! Completely Untested !!!

# SPDX-FileCopyrightText: 2021 Sandy Macdonald (Original PMK Advanced Keys)
# SPDX-FileCopyrightText: 2023 Martin Looker (rpico_rgb_keypad_obs v1.0.1 inserts)
## with Additional Crude Hacks to combine (UNTESTED!!!)
#
# SPDX-License-Identifier: MIT

# An advanced example of how to set up a HID keyboard.

# There are three layers, selected by pressing and holding key 0 (bottom left),
# then tapping one of the coloured layer selector keys above it to switch layer.

# The layer colours are as follows:

#  * layer 1: pink: numpad-style keys, 0-9, delete, and enter.
#  * layer 2: blue: sends strings on each key press
#  * layer 3: media controls, rev, play/pause, fwd on row one, vol. down, mute,
#             vol. up on row two

# You'll need to connect Keybow 2040 to a computer, as you would with a regular
# USB keyboard.

# Drop the `pmk` folder
# into your `lib` folder on your `CIRCUITPY` drive.

# NOTE! Requires the adafruit_hid CircuitPython library also!

import time
import math ## from OBS file
from pmk import PMK, number_to_xy, hsv_to_rgb ## from OBS file
## from pmk import PMK ## Duplicate ommitted
from pmk.platform.keybow2040 import Keybow2040 as Hardware          # for Keybow 2040
# from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware  # for Pico RGB Keypad Base

import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode

from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode

# Set up Keybow
keybow = PMK(Hardware())
keys = keybow.keys

# Set up the keyboard and layout
keyboard = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(keyboard)

# Set up consumer control (used to send media key presses)
consumer_control = ConsumerControl(usb_hid.devices)

# Our layers. The key of item in the layer dictionary is the key number on
# Keybow to map to, and the value is the key press to send.

# Note that keys 0-3 are reserved as the modifier and layer selector keys
# respectively.

layer_1 =     {4: Keycode.ZERO,
               5: Keycode.ONE,
               6: Keycode.FOUR,
               7: Keycode.SEVEN,
               8: Keycode.DELETE,
               9: Keycode.TWO,
               10: Keycode.FIVE,
               11: Keycode.EIGHT,
               12: Keycode.ENTER,
               13: Keycode.THREE,
               14: Keycode.SIX,
               15: Keycode.NINE}

layer_2 =     {} #use config for the OBS layer

layer_3 =     {6: ConsumerControlCode.VOLUME_DECREMENT,
               7: ConsumerControlCode.SCAN_PREVIOUS_TRACK,
               10: ConsumerControlCode.MUTE,
               11: ConsumerControlCode.PLAY_PAUSE,
               14: ConsumerControlCode.VOLUME_INCREMENT,
               15: ConsumerControlCode.SCAN_NEXT_TRACK}

layers =      {1: layer_1,
               2: layer_2,
               3: layer_3}

# Define the modifier key and layer selector keys
modifier = keys[0]

selectors =   {1: keys[1],
               2: keys[2],
               3: keys[3]}

# Start on layer 1
current_layer = 1

# The colours for each layer
colours = {1: (255, 0, 255),
           2: (0, 255, 255),
           3: (255, 255, 0)}

layer_keys = range(4, 16)

# Set the LEDs for each key in the current layer
for k in layers[current_layer].keys():
    keys[k].set_led(*colours[current_layer])

# To prevent the strings (as opposed to single key presses) that are sent from
# refiring on a single key press, the debounce time for the strings has to be
# longer.
short_debounce = 0.03
long_debounce = 0.15
debounce = 0.03
fired = False

## begin first insert from OBS file ##

# When true keycodes are sent
KC_LIVE = True

# LED Hues
HUE_SPLIT = (1.0/24.0)
hue = {
    "red"     : (HUE_SPLIT *  0.0),
    "rry"     : (HUE_SPLIT *  1.0),
    "ry"      : (HUE_SPLIT *  2.0),
    "ryy"     : (HUE_SPLIT *  3.0),
    "yellow"  : (HUE_SPLIT *  4.0),
    "yyg"     : (HUE_SPLIT *  5.0),
    "yg"      : (HUE_SPLIT *  6.0),
    "ygg"     : (HUE_SPLIT *  7.0),
    "green"   : (HUE_SPLIT *  8.0),
    "ggc"     : (HUE_SPLIT *  9.0),
    "gc"      : (HUE_SPLIT * 10.0),
    "gcc"     : (HUE_SPLIT * 11.0),
    "cyan"    : (HUE_SPLIT * 12.0),
    "ccb"     : (HUE_SPLIT * 13.0),
    "cb"      : (HUE_SPLIT * 14.0),
    "cbb"     : (HUE_SPLIT * 15.0),
    "blue"    : (HUE_SPLIT * 16.0),
    "bbm"     : (HUE_SPLIT * 17.0),
    "bm"      : (HUE_SPLIT * 18.0),
    "bmm"     : (HUE_SPLIT * 19.0),
    "magenta" : (HUE_SPLIT * 20.0),
    "mmr"     : (HUE_SPLIT * 21.0),
    "mr"      : (HUE_SPLIT * 22.0),
    "mrr"     : (HUE_SPLIT * 23.0),
}

# Hue:
#   Set this for the pad color.
#
# Group:
#   Set this to group pads together to operate like radio buttons (good for
#   scene selection). You can have many separate groups of keys as set by the
#   string set for the group
#
# Keycodes On:
#   These are the keyboard codes to be sent for normal, grouped and toggle on
#   pads.
#
# Keycodes Off:
#   These are the keyboard codes to be sent for toggle off pads, setting this
#   makes a toggle button, good for start/stop streaming
#
# Note:
#   Pads configured as toggles will be removed from any groups
#
config = [
    {}, # 0 ## changed from OBS file
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F14],                  "keycodes_off": None                        }, # 1
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F15],                  "keycodes_off": None                        }, # 2
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F16],                  "keycodes_off": None                        }, # 3
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F17],                  "keycodes_off": None                        }, # 4
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F18],                  "keycodes_off": None                        }, # 5
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F19],                  "keycodes_off": None                        }, # 6
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F20],                  "keycodes_off": None                        }, # 7
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F21],                  "keycodes_off": None                        }, # 8
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F22],                  "keycodes_off": None                        }, # 9
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F23],                  "keycodes_off": None                        }, # A
    {"hue": hue["magenta"], "group": "scene", "keycodes_on": [Keycode.F24],                  "keycodes_off": None                        }, # B
    {"hue": hue["cyan"]   , "group": None,    "keycodes_on": [Keycode.SHIFT,   Keycode.F13], "keycodes_off": [Keycode.SHIFT, Keycode.F13]}, # C
    {"hue": hue["cyan"]   , "group": None,    "keycodes_on": [Keycode.SHIFT,   Keycode.F14], "keycodes_off": [Keycode.SHIFT, Keycode.F14]}, # D
    {"hue": hue["green"]  , "group": None,    "keycodes_on": [Keycode.CONTROL, Keycode.F13], "keycodes_off": [Keycode.ALT,   Keycode.F13]}, # E
    {"hue": hue["red"]    , "group": None   , "keycodes_on": [Keycode.CONTROL, Keycode.F14], "keycodes_off": [Keycode.ALT,   Keycode.F14]}  # F
]

# LED Values (brightness)
VAL_SPLIT = (1.0/32.0)
VAL_MIN   = (VAL_SPLIT *  0.0)
VAL_OFF   = (VAL_SPLIT *  2.0)
VAL_ON    = (VAL_SPLIT * 30.0)
VAL_MAX   = (VAL_SPLIT * 32.0)
VAL_STEP  = 0.01

# Set up the keyboard and layout
## keyboard = Keyboard(usb_hid.devices) ## Duplicate not needed, but shouldn't be harmful
## layout = KeyboardLayoutUS(keyboard) ## Duplicate not needed, but shouldn't be harmful

# Set up Keybow
## keybow = PMK(Hardware()) ## Duplicate not needed, but shouldn't be harmful
## keys = keybow.keys ## Duplicate not needed, but shouldn't be harmful

# Add runtime data to config
for i in range(1, 16): ## changed from OBS file
    # Defaults
    # Mode is toggle
    config[i]["mode"] = None
    # Set LED value to max
    config[i]["val"] = VAL_MAX
    # Not down
    config[i]["down"] = False
    # Not on
    config[i]["on"] = False
    # This is a toggle pad ?
    if config[i]["keycodes_off"] != None and len(config[i]["keycodes_off"]) and len(config[i]["keycodes_on"]):
        # Mode is toggle
        config[i]["mode"] = "toggle"
        # Can't be in a group
        config[i]["group"] = None
    # This is a grouped pad ?
    if config[i]["group"] != None and len(config[i]["keycodes_on"]):
        # Mode is group
        config[i]["mode"] = "group"
    # This is a key pad ?
    if config[i]["mode"] == None and len(config[i]["keycodes_on"]):
        # Mode is key
        config[i]["mode"] = "key"
    # This key has not got a mode ?
    if config[i]["mode"] == None:
        # Set LED value to min (not lit)
        config[i]["val"] = VAL_MIN

# Presses a list of keycodes
def press_kcs(kcs):
    if current_layer == 2: ## changed from OBS file, next lines in block indented
        print(f'keycode press {kcs} {KC_LIVE}')
        if KC_LIVE:
            if len(kcs) == 1:
                keyboard.press(kcs[0])
            elif len(kcs) == 2:
                keyboard.press(kcs[0], kcs[1])
            elif len(kcs) == 3:
                keyboard.press(kcs[0], kcs[1], kcs[2])

# Releases a list of keycodes
def release_kcs(kcs):
    if current_layer == 2: ## changed from OBS file, next lines in block indented
        print(f'keycode release {kcs} {KC_LIVE}')
        if KC_LIVE:
            if len(kcs) == 1:
                keyboard.release(kcs[0])
            elif len(kcs) == 2:
                keyboard.release(kcs[0], kcs[1])
            elif len(kcs) == 3:
                keyboard.release(kcs[0], kcs[1], kcs[2])

# Process key presses/releases
for key in keys:
    # Pad pressed?
    @keybow.on_press(key)
    def press_handler(key):
        print(f'keypad press {key.number}')
        # Pad is now down
        config[key.number]["down"] = True
        # Normal pad ?
        if config[key.number]["mode"] == "key":
            # Press the on keycodes
            press_kcs(config[key.number]["keycodes_on"])
        # Toggle pad ?
        elif config[key.number]["mode"] == "toggle":
            # Toggle is currently on ?
            if config[key.number]["on"]:
                # Turn off
                config[key.number]["on"] = False
                # Press the off keycodes
                press_kcs(config[key.number]["keycodes_off"])
            # Toggle is currently off ?
            else:
                # Turn on
                config[key.number]["on"] = True
                # Press the on keycodes
                press_kcs(config[key.number]["keycodes_on"])
        # Grouped pad ?
        elif config[key.number]["mode"] == "group":
            # Turn on the pressed pad
            config[key.number]["on"] = True
            # Press the on keycodes
            press_kcs(config[key.number]["keycodes_on"])
            # Loop through pads
            for i in range(1, 16): ## changed from OBS file
                # Not the pad that has just been pressed ?
                if i != key.number:
                    # This pad is in the same group as the pad that has just been pressed ?
                    if config[i]["mode"] == "group" and config[i]["group"] == config[key.number]["group"]:
                        # The pad is on ?
                        if config[i]["on"]:
                            # Turn it off
                            config[i]["on"] = False
                            # Set val to minimum
                            config[i]["val"] = VAL_MIN

    # Pad released ?
    @keybow.on_release(key)
    def release_handler(key):
        print(f'keypad release {key.number}')
        # Pad is not down
        config[key.number]["down"] = False
        # Normal pad ?
        if config[key.number]["mode"] == "key":
            # Release on keycodes
            release_kcs(config[key.number]["keycodes_on"])
        # Toggle pad ?
        elif config[key.number]["mode"] == "toggle":
            # Pad has been toggled on ?
            if config[key.number]["on"]:
                # Release on keycodes
                release_kcs(config[key.number]["keycodes_on"])
            # Pad has just been turned off ?
            else:
                # Release off keycodes
                release_kcs(config[key.number]["keycodes_off"])
        # Grouped pad
        elif config[key.number]["mode"] == "group":
            # Release on keycodes
            release_kcs(config[key.number]["keycodes_on"])

    # Pad held ?
    @keybow.on_hold(key)
    def hold_handler(key):
        pass

## end first insert from OBS file ##

while True:
    # Always remember to call keybow.update()!
    keybow.update()

    # This handles the modifier and layer selector behaviour
    if modifier.held:
        # Give some visual feedback for the modifier key
        keys[0].led_off()

        # If the modifier key is held, light up the layer selector keys
        for layer in layers.keys():
            keys[layer].set_led(*colours[layer])

            # Change layer if layer key is pressed
            if current_layer != layer:
                if selectors[layer].pressed:
                    current_layer = layer

                    # Set the key LEDs first to off, then to their layer colour
                    for k in layer_keys:
                        keys[k].set_led(0, 0, 0)

                    for k in layers[layer].keys():
                        keys[k].set_led(*colours[layer])

    # Turn off the layer selector LEDs if the modifier isn't held
    else:
        for layer in layers.keys():
            keys[layer].led_off()

        # Give some visual feedback for the modifier key
        keys[0].set_led(0, 255, 25)

    # Loop through all of the keys in the layer and if they're pressed, get the
    # key code from the layer's key map
    for k in layers[current_layer].keys():
        if keys[k].pressed:
            key_press = layers[current_layer][k]

            # If the key hasn't just fired (prevents refiring)
            if not fired:
                fired = True

                # Send the right sort of key press and set debounce for each
                # layer accordingly (layer 2 needs a long debounce)
                if current_layer == 1:
                    debounce = short_debounce
                    keyboard.send(key_press)
                elif current_layer == 2:
                    ## Begin second insert from OBS file, with added indent for the whole insert ##
                    # Loop through pads
                    for i in range(1, 16):
                        # Start with LED off
                        h = 0.0
                        s = 0.0
                        v = 0.0
                        # No mode ?
                        if config[i]["mode"] == None:
                            # Turn off LED
                            keys[i].set_led(0, 0, 0)
                        # Pad has a mode ?
                        else:
                            # Pad is down ?
                            if config[i]["down"]:
                                # Normal or grouped pad
                                if config[i]["mode"] == "key" or config[i]["mode"] == "group":
                                    # Go to full brightness
                                    config[i]["val"] = v = VAL_MAX
                                # Toggle pad?
                                elif config[i]["mode"] == "toggle":
                                    # Toggled on ?
                                    if config[i]["on"]:
                                        # Go to full brightness
                                        config[i]["val"] = v = VAL_MAX
                                    # Toggled off ?
                                    else:
                                        # Go to min brightness
                                        config[i]["val"] = v = VAL_MIN
                            # Pad is not down
                            else:
                                # Pad is on
                                if config[i]["on"]:
                                    # Set target on brightness
                                    v = VAL_ON
                                # Pad is off ?
                                else:
                                    # Set target off brightness
                                    v = VAL_OFF
                            # Target value above current value ?
                            if v > config[i]["val"]:
                                # Move towards target
                                if v - config[i]["val"] > VAL_STEP:
                                    config[i]["val"] += VAL_STEP
                                else:
                                    config[i]["val"] = v
                            # Target value below current value
                            elif v < config[i]["val"]:
                                # Move towards target
                                if config[i]["val"] - v > VAL_STEP:
                                    config[i]["val"] -= VAL_STEP
                                else:
                                    config[i]["val"] = v
                            # Pad has a hue ?
                            if config[i]["hue"] is not None:
                                # Set full saturation
                                s = 1.0
                                # Set hue
                                h = config[i]["hue"]
                            # Convert the hue to RGB values.
                            r, g, b = hsv_to_rgb(h, s, config[i]["val"])
                 #           if i == 0:
                 #                print(f'{h} {s} {config[i]["val"]} {r} {g} {b} rgb')
                            # Finally set the LED
                            keys[i].set_led(r, g, b)
                    ## End second insert from OBS file ##
                elif current_layer == 3:
                    debounce = short_debounce
                    consumer_control.send(key_press)

    # If enough time has passed, reset the fired variable
    if fired and time.monotonic() - keybow.time_of_last_press > debounce:
        fired = False