Welcome back to the third and final part in our series showing developers how to work with non-blocking input in Python. In the first part of this series we learned how to collect input and event data with C/C++ and where the necessary files were located in Linux. In the second part of our tutorial series, we used Python to read keyboard events. In this final part, we will finally map event codes to keys and complete our example Python program.
You can read the first two part in our Python tutorial series here:
One final note before we continue: the code examples demonstrated in this programming tutorial series will use Python 3.9.2 and run on a Raspberry Pi 4 Model B (which you can purchase from the included link); however, this code should be portable to almost any Linux system which supports Python 3. To that end, this guide will include demonstrations of code in a Kali Linux environment running Python 3.9.12. The reason for choosing the Raspberry Pi as the primary demonstration device has more to do with extending the functionality of this code in a future article.
How to Read Keyboard Events in Python
So, while event codes are nice, it would be even nicer to map these events to keys on the keyboard. The key codes are constants which are stored in /usr/include/linux/input-event-codes.h. Below is a sampling of these values:
Figure 1 – Key Codes in Raspberry Pi OS
Figure 2 – Key Codes in Kali Linux
Now, while it normally is not a safe assumption that all the constants will always be the same values in every Linux environment, it is likely a “safer” assumption to make than most, as a lot of things would break if these values were different between Linux environments.
The previous Python code we worked on in the prior tutorial already includes one example of mapping. The ESC key is mapped from its key code of 1. However, other keys can also be mapped. The Python code below includes logic to parse out the mappings directly from /usr/include/linux/input-event-codes.h:
#demo-keyboard-mappings.py import struct import sys from datetime import datetime def GetKeyboardEventFile(tokenToLookFor): # Any exception raised here will be processed by the calling function. section = "" line = "" eventName = "" fp = open ("/proc/bus/input/devices", "r") done = False while False == done: line = fp.readline() if line: #print (line.strip()) if "" == line.strip(): #print ("\nFound Section:\n" + section) if -1 != section.find(tokenToLookFor) and -1 == section.lower().find("mouse"): # It is entirely possible there to be multiple devices # listed as a keyboard. In this case, I will look for # the word "mouse" and exclude anything that contains # that. This section may need to be suited to taste print ("Found [" + tokenToLookFor + "] in:\n" + section) # Get the last part of the "Handlers" line: lines = section.split('\n') for sectionLine in lines: # The strip() method is needed because there may be trailing spaces # at the end of this line. This will confuse the split() method. if -1 != sectionLine.strip().find("Handlers=") and "" == eventName: print ("Found Handlers line: [" + sectionLine + "]") sectionLineParts = sectionLine.strip().split(' ') eventName = sectionLineParts[-1] print ("Found eventName [" + eventName + "]") done = True section = "" else: section = section + line else: done = True fp.close() if "" == eventName: raise Exception("No event name was found for the token [" + tokenToLookFor + "]") return "/dev/input/" + eventName def MapCodeToKey (eventCodeStr): keyName = "" try: # What? You thought I was going to type in all the codes? fp = open ("/usr/include/linux/input-event-codes.h", "r") done = False while False == done and "" == keyName: line = fp.readline() if line: # Look for lines that only contain KEY_ as other constants have the same numbers as values. if -1 != line.strip().find("KEY_") and line.strip().endswith(eventCodeStr): # Crude but effective, just split the line by spaces and take the second value. lineParts = line.strip().split(' ') # Note that the line *may* be tab-delimited. keyName = lineParts.strip().split('\t') else: done = True fp.close() except Exception as err: # Not a deal-breaker, but an error should be reported. print ("Can't read from file /usr/include/linux/input-event-codes.h due to error [" + str(err) + "]") return keyName def main(argv): # Need to add code which figures out the name of this file from # /proc/bus/input/devices - Look for EV=120013 # Per Linux docs, 120013 is a hex number indicating which types of events # this device supports, and this number happens to include the keyboard # event. keyboardEventFile = "" try: keyboardEventFile = GetKeyboardEventFile("EV=120013"); except Exception as err: print ("Couldn't get the keyboard event file due to error [" + str(err) + "]") if "" != keyboardEventFile: try: k = open (keyboardEventFile, "rb"); # The struct format reads (small L) (small L) (capital H) (capital H) (capital I) # Per Python, the structure format codes are as follows: # (small L) l - long # (capital H) H - unsigned short # (capital I) I - unsigned int structFormat="llHHI" eventSize = struct.calcsize(structFormat) event = k.read(eventSize) goingOn = True while goingOn and event: (seconds, microseconds, eventType, eventCode, value) = struct.unpack(structFormat, event) # Per Linux docs at https://www.kernel.org/doc/html/v4.15/input/event-codes.html # Constants defined in /usr/include/linux/input-event-codes.h # EV_KEY (1) constant indicates a keyboard event. Values are: # 1 - the key is depressed. # 0 - the key is released. # 2 - the key is repeated. # The code corresponds to which key is being pressed/released. # Event codes EV_SYN (0) and EV_MSC (4) appear but are not used, although EV_MSC may # appear when a state changes. unixTimeStamp = float(str(seconds) + "." + str(microseconds)) utsDateTimeObj = datetime.fromtimestamp(unixTimeStamp) friendlyDTS = utsDateTimeObj.strftime("%B %d, %Y - %H:%M:%S.%f") if 1 == eventType: keyName = MapCodeToKey (str (eventCode)) # It is necessary to flush the print statement or else holding multiple keys down # is likely to block *output* print ("Event Size [" + str(eventSize) + "] Type [" + str(eventType) + "], code [" + str (eventCode) + "], key name [" + keyName + "], value [" + str(value) + "] at [" + friendlyDTS + "]", flush=True) if 1 == eventCode: print ("ESC Pressed - Quitting.") goingOn = False #if 4 == eventType: # print ("-------------------- Separator Event 4 --------------------") event = k.read(eventSize) k.close() except IOError as err: print ("Can't open keyboard input file due to the error [" + str(err) + "]. Maybe try sudo?") except Exception as err: print ("Can't open keyboard input file due to some other error [" + str(err) + "].") else: print ("No keyboard input file could be found.") return 0 if "__main__" == __name__: main(sys.argv[1:]) Listing 4 - Reading the keyboard Input and mapping it to actual keys.
Note that there is no “strange magic” here. The Python code is simply treating the C Header file as a text file and parsing it as such. It is not “compiling in” the values in the C Header file.
Below is some sample output, with annotations indicating specific key sequences being pressed:
Figure 3 – Sample Out with Key Names in Raspberry Pi
More sample output, this time in Kali Linux:
Figure 4 – Sample Out with Key Names in Kali Linux
Interpreting Key Sequences and Combinations in Python
As can be seen, combinations of keys pressed, such as modifiers including Shift, Ctrl, or Alt, can be captured along with the keys they modify. It is just necessary to note that all of the events pertaining to both keys being pressed must be captured and interpreted correctly. In the examples above, both events indicating that a modifier key has been depressed (1) or held down (2) are being used.
Also note that using a modifier key does not change the value of the key code that it modifies. For instance, the A key that is pressed along with Shift might produce a capital A when typed, but from the Operating System’s perspective, it is simply two keycodes being sent out as events.
Further, none of the code samples can capture Ctrl-Z, Ctrl-C or any other key sequence that either pushes the code execution into the background, or causes an interrupt exception, without adding further logic to handle such functions.
The examples which run on Kali make use of root access. However, as was shown on the Raspberry Pi device, this is not required (provided that the pi account was used). Note the group membership of the event files:
Figure 5 – Default Group Assignments of Input Event Files in Kali Linux
Notice how each of the event input files is assigned to the group “input”. The same group assignment is present on the Raspberry Pi device:
Figure 6 – Default Group Assignments of Input Event Files in Raspberry Pi OS
The difference is that the pi account on the Raspberry Pi device is part of the “input” group, but the “phil” account is not:
Figure 7 – “input” Group Members in Raspberry Pi OS, including the pi user account
Figure 8 – “input” Group Membership, with the phil account not present.
It is possible to simply assign a user a form of privileged access by assigning that user to the “input” group, but this can lead to other security issues, the most notable being that the files in /dev/input directory show any inputs generated by any user. While this may not be an issue with a single user logged into a desktop session, it can present other major security concerns if there are multiple users who share the same system. Do not forget that there are technologies that make it possible for multiple users to simultaneously get desktop access to a system. A user running these scripts with privileged access would be able to see what other users enter into the keyboard.
There do exist alternative libraries – ncurses being the most prominent example – which can provide non-root access to keyboard inputs, but these are beyond the scope of an introductory tutorial.
Final Thoughts on Python and Raspberry Pi Input
Being able to read keyboard input in a non-blocking manner in Python opens the door to being able to trap hotkeys and multi-key combinations that blocking programs cannot capture. Even the time intervals between key presses can be used to determine what sorts of things a program can do, such as performing an action faster or slower. Beyond just gaming, all sorts of other interactive applications can be developed using this coding structure, in spite of the restrictions imposed by platform dependency. Modern event-based coding in languages such as Java or C# is based in part on the concepts explored in this tutorial. And, on a purely editorial note, if a developer wanted to develop an application which used non-blocking inputs on the Windows platform, it would make more sense to use Java or C# for that purpose instead of Python, as those languages already have all the event-based hooks built in already.
But, in sticking back to Linux, the same logic presented in this article can apply to other devices whose files are listed in the /dev/input directory, although they do not return events in the same manner that the keyboard does. For instance, a gamepad plugged into the Raspberry Pi may return different event values each time the code is run to evaluate it, so further processing may be necessary to properly incorporate input from these devices into other programs.