Reading Keyboard Events with Python

by:

Softwares


In the last part of this tutorial series showing how to work with non-blocking input in Python, we learned how to capture input device data – such as keyboard events or mouse events – using a simple C++ script. We also learned whether that data was stored in a Linux system, as well as what character device files were.

You can revisit that Python tutorial by visiting: Intro to Non-Blocking Input in Python.

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.

So with all of this information at hand, it is now time to write a Python script to intercept the capture of this information. Such a script would need to be able to do two things:

  • Determine which event file to read.
  • Read the events from that file and parse them into something useful.

Determining the Keyboard Event File with Python

The example Python code below will parse the /proc/bus/input/devices file to determine which event file contains keyboard events. The contents of the file will be split into sections delimited by blank lines. Upon doing so, the code will search for the section which contains the term “EV=120013”. Once this section is found, the section will be further parsed to determine the proper event file.

Before we move on, however, a quick twist: during the composition of this tutorial, a very strange quirk came up. There were two sections of the /proc/bus/input/devices file which had the term “EV=120013.” So the idea of searching for this section flies out the window. Because of this quirk, it becomes necessary to illustrate both an incorrect and correct approach, because understanding how to work through such an issue is important to being able to successfully write Python code which can handle non-blocking inputs.

Most other tutorials that cover this topic assume that there exists only one device in a Linux environment that matches EV=120013, but the environments used for this article found two such devices; however, there was only one keyboard, so how would the determination be made in order to figure out what the correct file was?

Python Code Example: The Wrong Way

The code sample below shows one wrong way to get the keyboard event file using Python. This code is included because it is necessary to emphasize that certain core assumptions which a programmer may think are true, even to the point of orthodoxy, may not be the case:

# get-keyboard-event-file-wrong.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:
		# The loop control logic is intentionally done wrong here in order to
		# illustrate what happens when there are multiple devices with the same
		# EV identifier.
		line = fp.readline()
		if line:
			#print (line.strip())
			if "" == line.strip():
				#print ("\nFound Section:\n" + section)
				if -1 != section.find(tokenToLookFor):
					# 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="):
							print ("Found Handlers line: [" + sectionLine + "]")
							sectionLineParts = sectionLine.strip().split(' ')
							eventName = sectionLineParts[-1]
							print ("Found eventName [" + eventName + "]")
				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 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");
		print ("Keyboard Event File is [" + keyboardEventFile + "]")
	except BaseException as err:
		print ("Couldn't get the keyboard event file due to error [" + err + "]")
	return 0

if "__main__" == __name__:
	main(sys.argv[1:])

Listing 2 - The Wrong Way to Determine the Keyboard Event File

Below is the output on the Raspberry Pi device:

Figure 1 – The Wrong Keyboard Event File

A similar problem can also occur in Kali. It is safe to say that during the course of development, this issue must be anticipated and mitigated:

How to Read Input Files with Python

Figure 2 – Different OS, same wrong output

Note: At the time of this writing, Kali Linux still maps the Python command to the Python 2 interpreter. To use Python 3, the command python3 must be used.

Theoretically speaking, there should only be one device with the EV=120013 identifier, but in this example, there are two. Unfortunately this boils down to how each device attached to a system chooses to identify itself to Linux. In this case, extra logic is going to be needed in order to determine which file is the one that needs to be read.

In such situations, there are two ways to solve this problem:

  • Dig deep into the Linux Documentation and Source Code to figure out how Linux does this.
  • Make a reasonable guess.

Looking at the output, it is clear that the mouse, which identifies itself as a keyboard (but still works as a mouse), has the literal “Mouse” in its name. That being said, a better way to make this determination would be to exclude any section which includes “Mouse” in its heading, as the correct entry for the keyboard does not have this value in its name. This of course, would be a reasonable guess. It is not uncommon to have to devise and execute such ad-hoc approaches to solving problems like this, especially digging deep into the Linux Documentation and Source Code to figure out the correct way of doing this is not a practical option.

Read: Python: Basic Electronics Control with the Raspberry Pi

Python Code Example: The Right Way

The Python code below makes a few changes so that any section which contains “Mouse” is excluded, and it it also adds extra logic to stop once the “correct” file is determined:

# get-keyboard-event-file.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
				# Adding this section to show the extra section containing EV=120013
				elif -1 != section.find(tokenToLookFor) and -1 != section.lower().find("mouse"):
					print ("Found [" + tokenToLookFor + "] in the section below, but " +
						"it is not the keyboard event file:\n" + section)
				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 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");
		print ("Keyboard Event File is [" + keyboardEventFile + "]")
	except BaseException as err:
		print ("Couldn't get the keyboard event file due to error [" + err + "]")
	return 0

if "__main__" == __name__:
	main(sys.argv[1:])



Listing 2 - A Better Way to Determine the Keyboard Event File

All the alterations to our previous code do is ensure that the “reasonable guess” of “Mouse” not being in the section is enforced. Additionally, once the event file is found, no further processing of the contents of the /proc/bus/input/devices file takes place, so the elif… section may or may not be executed. Below is the output of this code on the Raspberry Pi device:

Read Keyboard input with Python

Figure 3 – Correctly Determining the Keyboard Input File, Raspberry Pi

Use Python to Read Keyboard Input

Figure 4 – Correctly Determining the Keyboard Input File, Kali

Now that the correct Keyboard Input Event File has been determined, the next step is to read the file and process it.

There is no reason that all event files that correspond to “EV=120013” could not be read simultaneously. Assuming that there is only one keyboard whose events are being read, there would still only be a single set of keyboard events to process.

A more extreme approach would be to forego looking for “EV=120013” altogether and simply read from all of the input event files in /dev/input, filtering only for a single set of keyboard events.

Read: Using Python for Basic Raspberry Pi Electronic Controls

How to Read Keyboard Events with Python

So, with all the ado out of the way, all that is needed is some basic binary data processing to read the raw data. The code below incorporates the determination of the Keyboard Input Event File and adds to it code to interpret the data:

# demo-keyboard.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 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 
				# 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:
					# 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) + "], 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 3 - Reading the keyboard Input

It is usually necessary to run this code as root. The reason why is because the files in /dev/input are owned by root. The best way to test this is to execute the code in a terminal window at the desktop, and, before pressing any keys, use the mouse to change focus away from the window. This way the output of the keys will not interfere with the display output of the script. It is also necessary to run this code directly on the desktop, not through VNC or SSH, as these servers do not pass keyboard events from a remote client to the Operating System.

To stop execution of this code, simply press the Escape key. Below is a sample output on the Raspberry Pi device, with the key codes highlighted:

Python Raspberry Pi Examples

Figure 5 – Sample output on the Raspberry Pi

Below is the sample output on Kali Linux. Observe how the size of the event is 24 bytes and not 16 bytes. This is an example of why it is important to dynamically calculate the size of the Keyboard Input Event before reading the data:

Raspberry Pi and Python examples

Figure 6 – Sample output in Kali Linux

In both examples, the value of 1 indicates that the key was pressed. A value of 0 indicates that it was released. Although it is not shown in the sample output above, a value of 2 indicates that a key is being held down.

It is up to the Environment to determine what constitutes the difference between simply pressing a key and holding it down.

Note, in addition to not assuming that the size of a Keyboard Input Event is constant, it is also important to assume that the names of the input event files are not constant as well. There is no reason why these cannot change any time a new peripheral is added to the system, or if some Operating System configuration change causes a change in the filename to occur.

Final Thoughts on Reading Keyboard Events in Python

That wraps up our second part in the tutorial series on how to work with non-blocking input in Python. In our third, and final part, we will look at how to map event codes to keys and wrap up our example program.



Source link

Leave a Reply

Your email address will not be published.