Search code examples
python-3.xraspberry-pi4hid

Raspberry pi impersonating a keyboard successfully, but can't send F13 and higher


I'm using a raspberry pi 4 to receive UDP packets and use that information to send key presses as if it's a usb keyboard. My code is below.

The code keeps track of keys the UDP packets tell it are held down and releases them when told. It also releases them if it's "held" beyond a certain duration. It's basically a remote keyboard/keystroke simulator.

It works fine for scan codes below 0x65. Scan codes above that don't work, notably F13 through F24. When I say "don't work" I mean it appears to be structured the same as the other scan codes as it leaves the raspberry pi (except with slightly higher numbers for that keycode), but the computer it's connected to does not respond as if keys are being pressed (it does for lower codes). I have a mouse that can send F13 through F24 successfully (although it uses special software...) so I don't think it's the receiver's shortcoming. The integer equivalent where it stops working is around 100, which might indicate some kind of intentional cutoff point. I knew nothing about working with HID before this, I suspect it's a problem within the import for open('/dev/hidg0', 'rb+') or hopefully I'm just structuring the bytes wrong. Please help me send F13, or at least tell me why I can't.

import time
import atexit
import binascii
import signal
#!/usr/bin/env python3
keys = { #keys I can refer to by name. Some are commented out for reasons
'error'.lower():0x01,
'A'.lower():0x04,
'B'.lower():0x05,
'C'.lower():0x06,
'd':0x07,
'E'.lower():0x08,
'F'.lower():0x09,
'G'.lower():0x0A,
'H'.lower():0x0B,
'I'.lower():0x0C,
'J'.lower():0x0D,
'K'.lower():0x0E,
'L'.lower():0x0F,
'M'.lower():0x10,
'N'.lower():0x11,
'O'.lower():0x12,
'P'.lower():0x13,
'Q'.lower():0x14,
'R'.lower():0x15,
'S'.lower():0x16,
'T'.lower():0x17,
'U'.lower():0x18,
'V'.lower():0x19,
'W'.lower():0x1A,
'X'.lower():0x1B,
'Y'.lower():0x1C,
'Z'.lower():0x1D,
'1':0x1E,
'2':0x1F,
'3':0x20,
'4':0x21,
'5':0x22,
'6':0x23,
'7':0x24,
'8':0x25,
'9':0x26,
'0':0x27,
'Enter'.lower():0x28,
'Escape'.lower():0x29,
'Backspace'.lower():0x2A, #possible ambiguity with delete
'Tab'.lower():0x2B,
'Space'.lower():0x2C,
'-':0x2D,
'+':0x2E,
'[':0x2F,
']':0x30,
'\\':0x31,
#'Non-US':0x32, #???
';':0x33,
'\'':0x34,
'`':0x35,
',':0x36,
'.':0x37,
'/':0x38,
'CapsLock'.lower():0x39,
'F1'.lower():0x3A,
'F2'.lower():0x3B,
'F3'.lower():0x3C,
'F4'.lower():0x3D,
'F5'.lower():0x3E,
'F6'.lower():0x3F,
'F7'.lower():0x40,
'F8'.lower():0x41,
'F9'.lower():0x42,
'F10'.lower():0x43,
'F11'.lower():0x44,
'F12'.lower():0x45,
'PrintScreen'.lower():0x46,
'ScrollLock'.lower():0x47,
'Pause'.lower():0x48,
'Insert'.lower():0x49,
'Home'.lower():0x4A,
'PgUp'.lower():0x4B,
'Delete'.lower():0x4C,
'End'.lower():0x4D,
'PgDn'.lower():0x4E,
'Right'.lower():0x4F,
'Left'.lower():0x50,
'Down'.lower():0x51,
'Up'.lower():0x52,
'NumLock'.lower():0x53,
'NumpadDiv'.lower():0x54,
'NumpadMult'.lower():0x55,
'NumpadSub'.lower():0x56,
'NumpadAdd'.lower():0x57,
'NumpadEnter'.lower():0x58,
'Numpad1'.lower():0x59, # and End
'Numpad2'.lower():0x5A, # and Down Arrow
'Numpad3'.lower():0x5B, # and PageDn
'Numpad4'.lower():0x5C, # and Left Arrow
'Numpad5'.lower():0x5D, #
'Numpad6'.lower():0x5E, # and Right Arrow
'Numpad7'.lower():0x5F, # and Home
'Numpad8'.lower():0x60, # and Up Arrow
'Numpad9'.lower():0x61, # and PageUp
'Numpad0'.lower():0x62, # and Insert
'NumpadDot'.lower():0x63, # and Del
#'Non-US Slash Bar':0x64, #???
#'Application':0x65, #??? some programs call this "menu" <---- this is the last one that works sequentially 
#'Power':0x66, #???
#'Keypad Equals':0x67,
'F13'.lower():0x68, #<---- I first noticed that these don't work at all :(
'F14'.lower():0x69,
'F15'.lower():0x6A,
'F16'.lower():0x6B,
'F17'.lower():0x6C,
'F18'.lower():0x6D,
'F19'.lower():0x6E,
'F20'.lower():0x6F,
'F21'.lower():0x70,
'F22'.lower():0x71,
'F23'.lower():0x72,
'F24'.lower():0x73,
#'Keypad Comma':0x85,
#'International1':0x87,
#'International2':0x88,
#'International3':0x89,
#'International4':0x8A,
#'International5':0x8B,
#'International6':0x8C,
#'LANG1':0x90,
#'LANG2':0x91,
#'LANG3':0x92,
#'LANG4':0x93,
#'LANG5':0x94,
'LCtrl'.lower():0xE0,
'LShift'.lower():0xE1,
'LAlt'.lower():0xE2,
'LWin'.lower():0xE3,
'RCtrl'.lower():0xE4,
'Rshift'.lower():0xE5,
'RAlt'.lower():0xE6,
'RWin'.lower():0xE7,
#'System Power Down':0x81,
#'System Sleep':0x82,
#'System Wake Up':0x83,
#'Scan Next Track':0x00B5,
#'Scan Previous Track':0x00B6,
#'Stop':0x00B7,
#'Play/Pause':0x00CD,
#'Mute':0x00E2,
#'Volume Increment':0x00E9,
#'Volume Decrement':0x00EA,
#'AL Consumer Control Configuration':0x0183,
#'AL Email Reader':0x018A,
#'AL Calculator':0x0192,
#'AL Local Machine Browser':0x0194,
'Browser_Search':0x0221,
'Browser_Home':0x0223,
'Browser_Back':0x0224,
'Browser_Forward':0x0225,
'Browser_Stop':0x0226,
'Browser_Refresh':0x0227,
'Browser_Previous':0x022A,
    }

    
def currenttime(): #for time stamping
    return round(time.time() * 1000)
    
def write_report(report): 
    with open('/dev/hidg0', 'rb+') as fd:
        fd.write(report) #how they're sent.
        
def debug(stringin): 
    if True: #to turn off all print spam
        print(stringin)

import socket

def exit_handler():
    write_report(bytearray([0,0,0,0,0,0,0,0])) #futile attempt to prevent keys from getting stuck on

UDP_IP = "0.0.0.0"
UDP_PORT = 5005

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
sock.setblocking(False)

write_report(bytearray([0,0,0,0,0,0,0,0]))

keydata = {} #which keys are held and their time stamps
shifting = False
controlling = False
alting = False
wining = False #windows key

atexit.register(exit_handler) #none of these really solved the problem, still won't trigger on x button clicked.
signal.signal(signal.SIGINT, exit_handler)
signal.signal(signal.SIGTERM, exit_handler)
while True:
    changemade = False
    key = "no key found"
    status = -1
    try:
        data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
    except BlockingIOError:
        for eachkey in keydata: #expiring
            if keydata[eachkey] < currenttime():
                keydata.pop(eachkey)
                changemade=True
                debug("change made by way of duration expiration"+" keydata "+str(keydata))
                break #remove only one per round because dictionaries can't handle the changes
    else:
        debug("received one...")
        entered = data.decode('UTF-8')
        if entered == 'NONE':
            write_report(bytearray([0,0,0,0,0,0,0,0]))
        if entered == 'terminate':
            break
        values = entered.split(" ", 2)
        
        key = values[0] #the name of the key according to keys[]
        status = int(values[1]) #0 or 1 for up or down
        duration = int(values[2]) #duration before key is automatically set to up
        if key not in keys:
            debug(key + ' not in keys')
            continue
        if key == 'lctrl' or key == 'rctrl':
            controlling = status == '1'
            debug('controlling =' + str(controlling))
        elif key == 'lshift' or key == 'rshift':
            shifting = status == '1'
            debug('shifting =' + str(shifting))
        elif key == 'lalt' or key == 'ralt':
            alting = status == '1'
            debug('alting =' + str(alting))
        elif key == 'lwin' or key == 'rwin':
            wining = status == '1'
            debug('wining =' + str(wining))
        else:
            if status == 0: #releasing
                if key in keydata.keys():
                    keydata.pop(key)
                    changemade=True
                    debug("change made by way of pop release"+" keydata "+str(keydata))
            if status == 1: #pressing
                if key in keydata:
                    key=key
                else:
                    keydata[key] = duration+currenttime()
                    changemade=True
                    debug("change made by way of push with new duration " + key + " " + str(duration)+" keydata "+str(keydata))
    if changemade:
        boolray = [controlling,shifting,alting,wining]
        binarios = sum(map(lambda x: x[1] << x[0], enumerate(boolray)))
        bytezors = bytearray([])
        bytezors.append(binarios) #modifier keys, shift, ctrl alt etc
        bytezors.append(0) #I think this space is for the right handed version of above
        iterator = 0
        for eachkey in keydata:
            bytezors.append(keys[eachkey]) #all keys that are held down
            iterator+=1
        for x in range(6-iterator):
            bytezors.append(0) #place holders if less than 6 keys held down
        write_report(bytezors) #send it off
        debug('result was ' + str(binascii.hexlify(bytezors)) + " key:" + key)
write_report(bytearray([0,0,0,0,0,0,0,0]))

UPDATE:

Searching through other people's attempts I found this https://forum.espruino.com/conversations/324014/

which links to this

https://www.espruino.com/modules/ble_hid_keyboard.js

which is for Arduino but it does contain this interesting comment: enter image description here

It sounds like what I'm using might not be on "maximum" mode, but I don't know how to adjust the settings for this. Unfortunately the link seen at the top of the screenshot 404s.

UPDATE AGAIN:

I initially used "key mime pie" to install the interface(?) that the code interacts with when it says open('/dev/hidg0', 'rb+'). I found the bash script they used to enable it.

Within this script there's a line:

echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > "${FUNCTIONS_DIR}/report_desc"

Which does what @aja described. I changed the corresponding values of x65 to x73 like the arduino example showed. I expected this to increase the maximum possible key identification, but instead I just get a BlockingIOError: [Errorno 11] write could not complete without blocking every time I try to send something.

My question is now what hex combination is required to make it work. Sadly I don't have a physical keyboard with F13 and beyond to spy on figure this out.


Solution

  • I figured it out. I followed this tutorial instead of the one mentioned in my question (intended for pizero):

    https://randomnerdtutorials.com/raspberry-pi-zero-usb-keyboard-hid/

    Additionally I edited /boot/config.txt by using sudo nano /boot/config.txt and added dtoverlay=dwc2.

    Instead of using the vendor and hardware IDs in the tutorial, I copied some from a different keyboard (with the manufacturer's full permission of course)

    Instead of using the tutorials hexcode, I substituted my own that expands the key set to f13 and beyond (the cmd that starts with echo -ne

    Instead of using the python code in the tutorial, I used my own above. And that worked. The blocking error appears to indicate something missing in this setup process, and the above is how I solved it, with a freshly installed OS.

    Edit: I tried redoing it from scratch following my own directions. I also needed to use the command chmod 777 /dev/hidg0 to make it work.