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:
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.
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.