The next step in getting the controller to work as a RetroPie controller is writing a driver so the console interprets the serial data coming in from the Bluetooth module the same way it would interpret a USB controller.
I got this idea from petRockBlog who mapped their custom, two-player joystick to two separate virtual controllers in RetroPie. Their code is available on GitHub. After a quick review, I saw that their code is using a C user input emulation library called uinput. In the interest of simplicity I found a Python uinput library and decided to use that to emulate my controller driver.
Below is a full snippet of the code from the GitHub repository followed by an explanation of how it works:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import serial | |
from uinput import * | |
class Controller: | |
"""docstring for Controller""" | |
NumButtons = 8 | |
def __init__(self, devName, devKeys): | |
self.devPath = "/dev/" + devName | |
try: | |
print "Connecting to " + self.devPath | |
self.serial = serial.Serial(self.devPath, 9600, timeout=10) | |
except Exception as inst: | |
print type(inst) # the exception instance | |
print inst.args # arguments stored in .args | |
print inst | |
print "Connection to " + self.devPath + " failed!" | |
self.connected = False | |
else: | |
print "Connection to " + self.devPath + " successful!" | |
self.connected = True | |
self.devKeys = devKeys | |
self.device = Device(devKeys) | |
ValidKeys = [ KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, | |
KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, | |
KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z,] | |
ControllerNames = ["rfcomm0"] | |
Controllers = [] | |
controllerNum = 0 | |
for controllerName in ControllerNames: | |
Controllers.append(Controller(controllerName, ValidKeys[controllerNum:((controllerNum*Controller.NumButtons) – 1)])) | |
Controllers[controllerNum].serial.reset_input_buffer() | |
controllerNum = controllerNum + 1 | |
PrevPacket = {} | |
PrevPacket['K'] = '1' * Controller.NumButtons | |
while True: | |
controllerNum = 0 | |
for controller in Controllers: | |
line = controller.serial.readline() | |
print line | |
packet = line.split(':') | |
if packet[0] == 'K': | |
charNum = 0 | |
for char, prevChar in zip(packet[1], PrevPacket['K']): | |
if (char == '0') and (prevChar == '1'): | |
controller.device.emit(controller.devKeys[(controllerNum*Controller.NumButtons) + charNum], 1) | |
elif (char == '1') and (prevChar == '0'): | |
controller.device.emit(controller.devKeys[(controllerNum*Controller.NumButtons) + charNum], 0) | |
charNum = charNum + 1 | |
PrevPacket['K'] = packet[1] | |
elif packet[0] == 'A': | |
pass | |
elif packet[0] == 'B': | |
pass | |
controllerNum = controllerNum + 1 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Controller: | |
"""docstring for Controller""" | |
NumButtons = 8 | |
def __init__(self, devName, devKeys): | |
self.devPath = "/dev/" + devName | |
try: | |
print "Connecting to " + self.devPath | |
self.serial = serial.Serial(self.devPath, 9600, timeout=10) | |
except Exception as inst: | |
print type(inst) # the exception instance | |
print inst.args # arguments stored in .args | |
print inst | |
print "Connection to " + self.devPath + " failed!" | |
self.connected = False | |
else: | |
print "Connection to " + self.devPath + " successful!" | |
self.connected = True | |
self.devKeys = devKeys | |
self.device = Device(devKeys) |
This is the definition of the Controller class. A static variable called NumButtons is used to show the maximum number of buttons a Controller has. A Controller instance is initialized with a path to the Bluetooth serial device which it then connects to. The initialization also creates a uinput Device that it will use to simulate a virtual controller for providing input to the console.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ValidKeys = [ KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, | |
KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, | |
KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z,] |
An array of keys is created to represent the possible input keys that will be passed into the system. Right now I’m just using the letters of the alphabet. RetroPie offers full controller customization so the actual keys used don’t really matter. I still plan on changing the controller driver to emulate a joystick with analog controls, however, so this method will need to be updated in the future.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ControllerNames = ["rfcomm0"] |
This array will store the device names for all of the Bluetooth serial consoles. At some point I’d like to autodetect all of the serial consoles from the controllers but for now a hardcoded array will work.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Controllers = [] | |
controllerNum = 0 | |
for controllerName in ControllerNames: | |
Controllers.append(Controller(controllerName, ValidKeys[controllerNum:((controllerNum*Controller.NumButtons) – 1)])) | |
Controllers[controllerNum].serial.reset_input_buffer() | |
controllerNum = controllerNum + 1 |
The program creates a controller for each serial console in the ControllerNames array and stores them in the Controllers array. It also resets the serial input buffer so old input data doesn’t bog down the driver and it can start processing the most current data.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
PrevPacket = {} | |
PrevPacket['K'] = '1' * Controller.NumButtons |
My driver operates on button transitions (unpressed->pressed and pressed->unpressed) rather than operating on the button state. This is to be consistent with keyboards which work in the same way. Because of this transition-based processing, the previous input packet needs to be stored so the previous packet can be compared to the current packet and differences between the two can be detected. Here the previous packet is instantiated as a dictionary and is initialized to a known, totally unpressed state.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
while True: | |
controllerNum = 0 | |
for controller in Controllers: | |
#… | |
controllerNum = controllerNum + 1 |
This is just an infinite loop which iterates over the list of controllers and processes each of them.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
line = controller.serial.readline() | |
print line | |
packet = line.split(':') |
A line is read from the serial port and split at the colon. The left side will be the key character which reflects the type of data and the right side will be the data itself.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if packet[0] == 'K': | |
#… | |
elif packet[0] == 'A': | |
#… | |
elif packet[0] == 'B': | |
#… |
This if statement detects which key is seen and executes the appropriate code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
charNum = 0 | |
for char, prevChar in zip(packet[1], PrevPacket['K']): | |
if (char == '0') and (prevChar == '1'): | |
controller.device.emit(controller.devKeys[(controllerNum*Controller.NumButtons) + charNum], 1) | |
elif (char == '1') and (prevChar == '0'): | |
controller.device.emit(controller.devKeys[(controllerNum*Controller.NumButtons) + charNum], 0) | |
charNum = charNum + 1 | |
PrevPacket['K'] = packet[1] |
This is the code for the key ‘K’ which is the only type of data I currently have. It iterates over each character in the eight bytes of data and compares it against the data from the previous packet. If the value went to ‘1’ from ‘0’ it emits an event to signal that the key was pressed and if the value went to ‘0’ from ‘1’ it emits an event to signal the opposite.