This project demonstrates how to build a voice-enabled video game controller, designed to help users with mobility impairments interact with games using voice commands.
In addition to a joystick and multiple buttons, the controller includes a microphone and voice processor, allowing it to listen for and interpret game-related commands spoken by the user.
Gamers with disabilities that limit their use of traditional controllers can use this voice-enabled alternative to accommodate their unique needs, enabling them to access and play a larger library of games.
Build2gether ChallengeThe genesis of this project comes from the Build2gether Inclusive Innovation Challenge, which calls for participants to build "innovative solutions to help individuals with disabilities overcome their daily struggles".
Three solution themes are specified in the challenge, including gaming for people with mobility impairments. This theme was intriguing to me because I regularly play video games and have experimented with building gaming hardware in the past.
To help better understand the needs of gamers with disabilities, the challenge provided the opportunity to interact with Contest Masters, a group of representatives who experience mobility issues firsthand.
Through discussions with the Contest Masters on Discord, I was able to learn about specific obstacles faced by disabled gamers, develop an idea that fills a gap in current hardware solutions, and receive multiple rounds of feedback while building a device prototype.
Problem IdentificationThe Contest Masters played a crucial role in the problem-identification process by sharing how their own disabilities, such as Duchenne Muscular Dystrophy and Spinal Muscular Atrophy Type II, personally impact their ability to play games.
What were the needs or pain points that you attended to and identified when you were solving problems faced by the Contest Masters?
Conversations with the Contest Masters revealed two significant pain points for gamers with disabilities:
1) Controller Manipulation: Gamers with motor-related disabilities often lack the physical strength or dexterity to operate a standard controller.
Examples of this include gamers having fingers too weak to press buttons, having limited mobility to move a joystick, and needing to use the controller with just one hand.
While some third-party attachments can assist with controller manipulation, they don't address the wide range of limitations that exist for these gamers.
2) Input Mapping: Disabled gamers need the ability to map controller inputs to the in-game actions of their choice.
Given their limited controller handling, gamers depend on customizing which actions are triggered by the buttons and joysticks they can still use. This option enables them to play games according to their individual capabilities.
Not all games include built-in support for custom input mapping, leaving them inaccessible to many gamers with disabilities.
Developing a SolutionI began by generating ideas for solutions that would address these pain points by leveraging my background in hardware and electronics.
This process led me to design a new form of video game controller with two innovative features:
1) Voice Recognition: The controller listens for and recognizes voice commands from users, which serve as controller inputs.
If a gamer encounters difficulties using a specific button or joystick, they can instead speak commands into the controller's built-in microphone to trigger the same in-game actions.
Gamers have the flexibility to use voice commands, physical inputs, or a combination of both, in order to accommodate their unique mobility needs.
2) Custom Input Mapping: The controller can be customized by users to map voice commands and physical inputs to in-game actions of their choice.
Input mappings are defined within the controller's software, which can be easily updated by the user to meet their specific needs. The joystick, tactile buttons, and voice commands are individually configurable for full customization.
Video games that lack built-in support for input mapping are now accessible to disabled games by using this feature of the controller.
How It WorksThe controller can be connected to any computer to function as an input device for playing computer-based video games.
In addition to its standard controller features, which include an analog joystick and six tactile buttons, the controller is equipped with a built-in OLED display and microphone for added functionality.
To play video games, users simply connect the controller to the computer via a USB cable. This delivers power to the controller and establishes communication with the computer.
When connected to the computer, the controller emulates a keyboard and transmits specific keystroke commands for each user input. These keystrokes can include alphanumeric characters as well as special keys (e.g., Up Arrow).
Similar to a keyboard serving as an input device for video games, this controller functions in the same way. The keystrokes sent to the computer are relayed to the video game, which interprets them as in-game actions.
For an in-game example, pressing the blue button can send a "space bar" keystroke to the computer, the input for a character "jump" action. Or, pressing the joystick forward can send an "up arrow" keystroke, the input for a "move forward" action.
The buttons and joystick operate like those on a traditional controller. Each press or movement initiates a keystroke, and when the button or joystick returns to its initial position the keystroke is released.
Users who struggle to reach or manipulate the buttons or joystick can instead use voice commands to send the same keystroke inputs to the video game.
A microphone animation is displayed on the OLED to indicate that the controller is actively listening for voice command inputs.
There are seven game-related voice commands that the controller can recognize after being trained by the user: Jump, Run, Shoot, Up, Down, Left, and Right.
When a user speaks a command and it is detected by the controller, the controller sends its corresponding keystroke to the computer, and the OLED displays a unique command icon.
For example, when a user says "shoot", a "space bar" keystroke is sent to the computer to make the video game character jump, and the OLED flashes a "jump" icon.
Voice commands and physical inputs can be used in any combination to interact with a game. This provides gamers the flexibility to use the controller according to their individual needs.
The keystroke that is transmitted with each physical and voice input can be customized within the controller's software.
Users can access the controller's programming script on the connected computer to update keystroke assignments for each input. Additionally, the duration for which the keystroke is held for voice inputs can be defined.
This custom input mapping ensures that essential game actions can be triggered by the physical inputs accessible to each individual user.
For example, if a user can only manipulate the two center buttons on the controller, the keystrokes for a game's required "jump" and "shoot" actions can be assigned to those buttons.
Since these mappings are entirely defined within the controller's onboard software and determine what is transmitted to the computer, they work independently of any game.
This customization option makes it possible for users with limited mobility to access and play games that do not natively support custom input mapping, which is a crucial feature for their gaming experience.
Controller HardwareThe controller consists of hardware and electronics enclosed within a 3D-printed body, designed to resemble a traditional gaming controller.
An Arduino Micro provides the controller's processing power and executes an Arduino sketch responsible for capturing user inputs, transmitting keystrokes, and interfacing with all other components.
Voice recognition capabilities are enabled by an Elechouse Voice Recognition Module V3. This component is trained to recognize commands from the user's voice and actively listen for them while the controller is in use.
Physical input components for the controller include an analog joystick and six tactile buttons. An OLED display is incorporated into the controller to deliver visual feedback to users as they speak voice commands.
Each component is secured to mounting holes on the face or back plate of the controller body, and jumper wires are soldered between the components and their corresponding pins on the Arduino Micro.
The two halves of the controller body are fastened together, and a microphone is plugged into the 3.5mm audio jack on the top of the controller.
Once fully assembled, the controller can be connected to a computer via a USB plugged into its USB port. This connection will power on the controller and initiate the sketch on the Arduino Micro.
Build InstructionsBelow are instructions on how to test, train, and build the voice-enabled video game controller. These instructions assume you can access the Arduino IDE on a computer and are familiar with writing and uploading Arduino sketches.
Complete project code and 3D print files be found within this project's attachments and on GitHub.
Prototype CircuitTo help troubleshoot component connections and programming issues before final assembly, a prototype of the controller is first built on a breadboard.
1. Place components on a breadboard, then use jumper wires to create the connections between components as seen in this circuit diagram:
The prototype layout should resemble the following:
2. Connect the Arduino Micro to the computer via a USB cable.
This connection will power the board and allow Arduino sketches to be uploaded to it during the build process.
Arduino LibrariesIn order for the Arduino sketch to operate the OLED display and voice recognizer, it requires additional code from two external libraries. These libraries need to be installed in the Arduino IDE to be used.
1. Open the Arduino IDE on a computer and navigate to the Library Manager (Tools -> Tools Library Manager).
2. In the Manager, search for the Adafruit_SSD1306
and click "Install".
The Adafruit_SSD1306
library enables interactions with the controller's SSD1306 OLED display, including drawing text and graphics on it.
3. Visit the Elechouse website and click the "Library for Arduino" download link to download a zip file named VoiceRecognitionV3.zip
.
4. Back in the Arduino IDE, go to "Sketch", then "Include Library", and select "Add.ZIP Library."
5. When prompted, choose the VoiceRecognitionV3.zip
file downloaded earlier to complete the installation.
The VoiceRecognitionV3
library works with the Voice Recognition Module, providing code to capture and detect voice commands.
The controller's Arduino sketch will be developed feature by feature in the sections below. A blank sketch is used as a template to build upon.
1. In the Arduino IDE, navigate to "Examples", then "01.Basics", and select "BareMinimum".
This sketch contains the bare minimum code required for a valid Arduino sketch, which is both a setup()
and loop()
function.
2. Add the following code inside the setup() function:
// Initialize serial communication
Serial.begin(115200);
Here a serial communication is established between the Arduino Micro and the computer, allowing information to be printed to the IDE's serial terminal.
Button InputsSix tactile buttons are available as physical inputs on the controller. These buttons can be pressed and held to trigger an input, then let go to release it, functioning similarly to traditional controllers.
The Arduino sketch is responsible for detecting when the buttons are pressed and released, and transmitting the corresponding keystroke to the computer.
1. Include the Keyboard
library at the top of the project's sketch:
#include "Keyboard.h"
This library provides the capability to programmatically send key press and release commands to a connected computer, allowing the Arduino to emulate a keyboard.
2. Add the following code above the setup()
function:
// Game button structure
struct GameButton {
int pin;
int key;
bool isPressed;
};
To process each button input, three pieces of information are required: the Arduino pin it's connected to (pin
), the keyboard key to transmit when pressed (key
), and its current state of being pressed or not (isPressed
).
A GameButton
structure is created to represent a single button and the data values that define it.
3. Define input mappings for each button:
// Button input mappings
int button1Key = 105;
int button2Key = 101;
int button3Key = 97;
int button4Key = 32;
int button5Key = 176;
int button6Key = 122;
Here, the keyboard inputs triggered by each individual button are defined.
These inputs are represented as ASCII code integers, corresponding to a specific alphanumeric or special keyboard key. For example, the key for Button 4 is 32
, representing the space bar
on the keyboard.
Users can modify these variables with any ASCII value to customize the button mapping for their gaming needs. After making changes, uploading the sketch to the controller will instantly apply the updated mapping.
4. Define each of the six input buttons:
// Game buttons array
const int numButtons = 6;
GameButton gameButtons[numButtons] = {
{4, button1Key, false}, // Button 1
{5, button2Key, false}, // Button 2
{6, button3Key, false}, // Button 3
{7, button4Key, false}, // Button 4
{8, button5Key, false}, // Button 5
{9, button6Key, false} // Button 6
};
An array of GameButton
structures called gameButtons
is created to hold the data defining each of the six buttons.
The pin
values correspond to the pin numbers specified in the circuit diagram, the key
values are derived from the mapping definitions, and each isPressed
value is set to false
by default.
5. Within the sketch's setup()
function, add the following line to initialize the keyboard emulation:
// Initialize keyboard output
Keyboard.begin();
6. Define a processButtons()
function at the end of the sketch:
void processButtons() {
// Loop through all buttons
for (int i = 0; i < numButtons; i++) {
// Read button digital value - LOW == Pressed
int buttonVal = digitalRead(gameButtons[i].pin);
// Press key on button press
if (buttonVal == LOW && gameButtons[i].isPressed == false) {
Keyboard.press(gameButtons[i].key);
gameButtons[i].isPressed = true;
Serial.println("Button pressed");
}
// Release key on button release
if (buttonVal == HIGH && gameButtons[i].isPressed == true) {
Keyboard.release(gameButtons[i].key);
gameButtons[i].isPressed = false;
Serial.println("Button released");
}
}
}
7. Then call this function within the sketch's loop()
function:
// Process button inputs
processButtons();
The processButtons()
function is responsible for managing the user's interactions with all six buttons.
It accomplishes this by iterating through the gameButtons
array, and checking if each button has been pressed or released using its current pin
value reading and isPressed
state.
When a button is pressed, Keyboard.press()
is called, passing the button's key
value, simulating holding down a keyboard key. Similarly, when a button is released, Keyboard.release()
is called, releasing the held key.
After either action, the button is updated to reflect its change in state and a button press/release message is printed to the serial console.
The function is called on each iteration of the loop()
function, ensuring that the user's button interactions are continuously processed.
Like the buttons, the analog joystick serves as a physical input on the controller. Keyboard inputs are triggered when the stick is pressed up, down, left, or right, and they are released when it's returned to the center.
The Arduino sketch detects the position of the stick and transmits the appropriate keystrokes to the computer.
1. Add the following code above the setup()
:
// Joystick pins
int xPin = A0;
int yPin = A1;
This code block defines the Arduino board pins used by the joystick. A dual-axis joystick requires a connection to two analog pins in order to read its position values in both the X and Y directions.
2. Define the input mapping for the joystick:
// Joystick input mapping
int upKey = KEY_UP_ARROW;
int downKey = KEY_DOWN_ARROW;
int leftKey = KEY_LEFT_ARROW;
int rightKey = KEY_RIGHT_ARROW;
Keyboard inputs that are triggered by the up, down, left, and right stick positions are defined here. The default values are each constants representing special keyboard keys, in this case the four directional keys.
3. Set the joystick states:
// Joystick states
bool upPressed = false;
bool downPressed = false;
bool leftPressed = false;
bool rightPressed = false;
These variables track the state of the joystick's position in each direction and are set to false
by default.
6. Define a processJoystick()
function at the end of the sketch:
void processJoystick() {
// Read X and Y analog values
int yValue = analogRead(yPin);
int xValue = analogRead(xPin);
// Map values between 0-100
int xMapped = map(xValue, 0, 1023, 0, 100);
int yMapped = map(yValue, 0, 1023, 0, 100);
// Apply X keyboard inputs
if (xMapped < 30) {
if (leftPressed == false) {
leftPressed = true;
Keyboard.press(leftKey);
Serial.println("Left pressed");
}
} else if (xMapped > 60) {
if (rightPressed == false) {
rightPressed = true;
Keyboard.press(rightKey);
Serial.println("Right pressed");
}
} else {
if (leftPressed == true) {
leftPressed = false;
Keyboard.release(leftKey);
Serial.println("Left released");
}
if (rightPressed == true) {
rightPressed = false;
Keyboard.release(rightKey);
Serial.println("Right released");
}
}
// Apply Y keyboard inputs
if (yMapped < 30) {
if (downPressed == false) {
downPressed = true;
Keyboard.press(downKey);
Serial.println("Down released");
}
} else if (yMapped > 60) {
if (upPressed == false) {
upPressed = true;
Keyboard.press(upKey);
Serial.println("Up pressed");
}
} else {
if (downPressed == true) {
downPressed = false;
Keyboard.release(downKey);
Serial.println("Down released");
}
if (upPressed == true) {
upPressed = false;
Keyboard.release(upKey);
Serial.println("Up released");
}
}
}
5. Then call this function within the sketch's loop()
function:
// Process joystick inputs
processJoystick();
The processJoystick()
function handles the user's interactions with the joystick on each iteration of the loop()
function.
It first determines the joystick's current position by reading the analog values from the xPin
and yPin
, and mapping the values between 0 and 100.
An xMapped
value of 0 or 100 indicates the stick is fully left or right, respectively, while a yMapped
value of 0 or 100 indicates the stick is fully up or down. A value of 50 represents the stick's center in both directions.
Next, the function checks if the stick has moved in or out of the up, down, left, and right positions using a position threshold value and the pressed state of each direction.
When a new position is initiated, Keyboard.press()
is called using that direction's key value. Likewise, a call to Keyboard.release()
releases the key when the stick leaves the position.
A move in or out of one of the four positions will update its current pressed state and print a press/release message to the serial console.
Voice Recognition TrainingThe voice recognition capabilities of the controller are provided by the voice recognition module, which can listen for and detect spoken voice commands.
To enable this functionality, the module must first be trained on the user's voice speaking the specific commands that the controller will utilize.
Included in the VoiceRecognitionV3
library is a training sketch that facilitates training and loading commands onto the module for use.
1. In the Arduino IDE, go to "File", then "Examples", then "VoiceRecognitionV3", and select "vr_sample_train".
This opens the vr_sample_train sketch
within a new IDE editor window.
2. Update line 36 to the following:
VR myVR(10,11);
The default pin numbers (2 & 3) in this statement must be updated to match the pins used by the module in the prototype circuit (10 & 11).
3. Upload this sketch to the connected Arduino Micro, and open the Serial Monitor with the baud rate set to 115200.
When the sketch initiates, it will print a help menu listing the command line inputs that the user can execute to the Serial Montior.
There are seven voice commands that the controller will be programmed to accept: Jump, Run, Shoot, Up, Down, Left, and Right. Each of these commands will be trained in order.
4. In the Serial Monitor input, enter the following: train 01 2 3 4 5 6
This command initiates the training sequence for the seven voice commands, which involves following prompts printed to the console directing the user when to speak each command.
5. When prompted to "Speak now" by the instructions, say the word "Jump". Repeat the word "Jump" when prompted to "Speak again".
If there is a mismatch between the two spoken voice examples, the training will prompt another attempt. This will repeat as long as there is a mismatch.
When the two examples match, "Success" will be printed, indicating that "Jump" has been successfully trained.
6. Repeat Step 5 as prompted, progressing through the rest of the commands: "Run", "Shoot", "Up", "Down", "Left", and "Right", until training is complete.
7. In the serial monitor input, enter the following: load 01 2 3 4 5 6
This command loads the seven trained voice commands onto the module, enabling them to be programmatically detected within the controller's sketch.
Voice Command InputsSeven game-related voice commands are accepted as voice inputs by the controller. Each command triggers a keyboard input when it is detected, which is held for a set amount of time before being released.
The Arduino sketch is responsible for interfacing with the voice recognition module to process audio from the microphone and determine if a command was spoken, then transmit the corresponding keystroke to the computer.
1. Include the VoiceRecognitionV3
library at the top of the project's sketch:
#include "VoiceRecognitionV3.h"
This library includes additional code that enables the sketch to control and utilize the Elechouse voice recognition module.
2. Add the following code above the setup()
function:
// Voice recogniztion module object
VR myVR(10,11);
An instance of the VR
voice recognition object class is created, using the component's RX/TX pins. This object includes various methods for processing audio and detecting trained commands.
3. Define a data buffer:
// Voice recognition buffer
uint8_t buf[64];
When voice commands are detected, multiple pieces of data about the command are returned, which will be stored in this buffer variable.
4. Define a voice command structure:
// Voice command structure
struct VoiceCommand {
String name;
int key;
unsigned long startMillis;
unsigned long duration;
bool isPressed;
};
To process each voice input, multiple pieces of information are required: the command word (name
), the keyboard key to transmit when detected (key
), the time it was detected (startMillis
), the duration of the key press (duration
), and its current state of being pressed or not (isPressed
).
A VoiceCommand
structure is defined to represent a single voice command and the data values that it consists of.
5. Define input mappings for each button:
// Voice command input mappings
int jumpCommandKey = 32;
int runCommandKey = 101;
int shootCommandKey = 97;
int upCommandKey = KEY_UP_ARROW;
int downCommandKey = KEY_DOWN_ARROW;
int leftCommandKey = KEY_LEFT_ARROW;
int rightCommandKey = KEY_RIGHT_ARROW;
Key inputs that are triggered by each of the seven voice commands are defined here. The default values include ASCII code numbers for alphanumeric keys and constants for the four directional keyboard keys.
Users can edit these variables with any key values to customize the voice command mapping for their individual needs, or to match the game actions the name command is referencing.
6. Define each of the seven voice commands:
// Game voice commands
const int numCommands = 7;
VoiceCommand voiceCommands[numCommands] = {
{"Jump", jumpCommandKey, 0, 500, false}, // Jump
{"Run", runCommandKey, 0, 500, false}, // Run
{"Shoot", shootCommandKey, 0, 250, false}, // Shoot
{"Up", upCommandKey, 0, 1000, false}, // Up
{"Down", downCommandKey, 0, 1000, false}, // Down
{"Left", leftCommandKey, 0, 1000, false}, // Left
{"Right", rightCommandKey, 0, 1000, false} // Right
};
An array of VoiceCommand
structures called voiceCommands
is created to hold the data defining each of the seven commands.
The name
values correspond to each trained command and key
values come from the mapping definitions.
Each startTime
value is initialized as 0, and the duration
is between 250 and 1000 milliseconds depending on the command. The isPressed
value is set to false
by default for each command.
7. Inside the sketch's setup()
function add the following:
// Initialize voice recognition
myVR.begin(9600);
This line initializes the voice recognition object and opens up communication between the board and the module.
8. Add the following code blocks inside the setup()
function:
// Clear voice recognizer
if(myVR.clear() == 0){
Serial.println("Recognizer cleared.");
}else{
Serial.println("Voice Recognition Module not found.");
while(1);
}
// Load trained voice records
for (int i = 0; i < numCommands; i++) {
VoiceCommand command = voiceCommands[i];
myVR.load((uint8_t)i);
Serial.println(String(command.name) + " loaded");
}
The voice recognition object must be cleared, and then loaded with the seven voice commands loaded onto the module.
9. Define a processVoice()
function at the end of the sketch:
void processVoice() {
// Perform voice recognition
int ret = myVR.recognize(buf, 50);
// Process voice record match
if (ret > 0) {
int index = buf[1];
if (voiceCommands[index].isPressed == false) {
Serial.println(String(voiceCommands[index].name) + " command recognized");
Keyboard.press(voiceCommands[index].key);
voiceCommands[index].isPressed = true;
voiceCommands[index].startMillis = millis();
}
}
// Process no voice record match
else {
unsigned long currentMillis = millis();
for (int i = 0; i < numCommands; i++) {
if (voiceCommands[i].isPressed == true && currentMillis - voiceCommands[i].startMillis >= voiceCommands[i].duration) {
Keyboard.release(voiceCommands[i].key);
voiceCommands[i].isPressed = false;
}
}
}
}
10. Then call this function within the sketch's loop()
function:
// Process voice recognizer
processVoice();
On each iteration of the loop()
function, processVoice()
is called to handle and process voice commands from the user.
First, the recognize()
method of the voice recognition object is called, which returns a value indicating if one of the seven voice commands has been recognized by the module.
If a command has been spoken and recognized, indicated by a positive ret
value, the index
of the corresponding VoiceCommand
is obtained, and a Keyboard.press()
is initiated for that command's mapped key.
In addition, the command's isPressed
state is set to true
, and the startMillis
value is set as the current computer time. A message detailing which command was recognized is printed to the serial monitor.
When no command has been recognized by the module, the code loops through each VoiceCommand
, checking if its key is currently pressed and if its key hold duration
has been surpassed.
If both those values checks are true, a Keyboard.release()
is transmitted for the command's key, and its isPressed
state is reset to false
.
The OLED display on the controller provides visual feedback to indicate when the controller is actively listening for voice commands and when it has successfully recognized one.
Code within the Arduino sketch interfaces with the OLED to display a listening animation as well as custom icons for each of the seven voice commands.
1. Include the Adafruit_SSD1206
library at the top of the project's sketch:
#include <Adafruit_SSD1306.h>
This library contains additional code that enables interacting with the SSD1306 OLED display, including drawing graphics on it.
2. Add the following above the sketch's setup()
function:
// Microphone bitmap
int micBitmapSize = 32;
const unsigned char micBitmap [] PROGMEM = {
0x00, 0x03, 0xe0, 0x00, 0x00, 0x0f, 0xf8, 0x00, 0x00, 0x1c, 0x1c, 0x00, 0x00, 0x18, 0x0c, 0x00,
0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00,
0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x30, 0x06, 0x00,
0x00, 0x30, 0x06, 0x00, 0x03, 0x30, 0x06, 0x60, 0x03, 0x30, 0x06, 0x60, 0x03, 0x30, 0x06, 0x60,
0x03, 0x30, 0x06, 0x60, 0x03, 0x30, 0x06, 0x60, 0x01, 0x98, 0x0c, 0x60, 0x01, 0x9c, 0x1c, 0xc0,
0x01, 0xcf, 0xf8, 0xc0, 0x00, 0xc3, 0xf1, 0x80, 0x00, 0xe0, 0x03, 0x80, 0x00, 0x38, 0x0f, 0x00,
0x00, 0x1f, 0xfc, 0x00, 0x00, 0x07, 0xf0, 0x00, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x01, 0xc0, 0x00,
0x00, 0x01, 0xc0, 0x00, 0x00, 0x01, 0xc0, 0x00, 0x00, 0xff, 0xff, 0x80, 0x00, 0xff, 0xff, 0x80
};
// Jump icon bitmap
const unsigned char jumpBitmap [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x04, 0x40, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x08, 0x80, 0x00, 0x00, 0x00, 0x02, 0x10, 0x08, 0x80,
0x00, 0x00, 0x00, 0x04, 0x08, 0x11, 0x00, 0x00, 0x00, 0x00, 0x08, 0x04, 0x11, 0x00, 0x00, 0x00,
0x00, 0x08, 0x04, 0x22, 0x00, 0x00, 0x00, 0x00, 0x08, 0x04, 0x22, 0x00, 0x00, 0x00, 0x00, 0x08,
0x04, 0x44, 0x80, 0x00, 0x00, 0x00, 0x08, 0x04, 0x88, 0x80, 0x00, 0x00, 0x00, 0x04, 0x09, 0x08,
0x80, 0x00, 0x00, 0x00, 0x02, 0x11, 0x10, 0x80, 0x00, 0x00, 0x00, 0x01, 0xe2, 0x10, 0x80, 0x00,
0x00, 0x00, 0x00, 0x0c, 0x20, 0x80, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x40, 0x80, 0x00, 0x00, 0x00,
0x07, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x18, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0xe0, 0x01,
0x08, 0x80, 0x00, 0x00, 0x01, 0x00, 0x01, 0x08, 0x80, 0x00, 0x00, 0x02, 0x00, 0x01, 0x08, 0x80,
0x00, 0x00, 0x04, 0x1c, 0x01, 0x08, 0x80, 0x00, 0x00, 0x04, 0x22, 0x01, 0x08, 0x80, 0x00, 0x00,
0x04, 0x42, 0x01, 0x00, 0x80, 0x00, 0x00, 0x04, 0x42, 0x01, 0x00, 0x80, 0x00, 0x00, 0x02, 0x22,
0x01, 0x00, 0x80, 0x00, 0x00, 0x02, 0x22, 0x01, 0x00, 0x80, 0x00, 0x00, 0x01, 0x12, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x00, 0x80, 0x00, 0x00,
0x00, 0x00, 0x4a, 0x00, 0x40, 0x00, 0x00, 0x00, 0x04, 0x32, 0x00, 0x30, 0x00, 0x00, 0x00, 0x04,
0x02, 0x18, 0x0c, 0x00, 0x00, 0x00, 0x04, 0x02, 0x26, 0x02, 0x00, 0x00, 0x00, 0x04, 0x82, 0x21,
0x01, 0x00, 0x00, 0x00, 0x04, 0x84, 0x20, 0xc1, 0x00, 0x00, 0x00, 0x04, 0x84, 0x20, 0x21, 0x00,
0x00, 0x00, 0x04, 0x84, 0x20, 0x41, 0x00, 0x00, 0x00, 0x04, 0x84, 0x40, 0x81, 0x00, 0x00, 0x00,
0x04, 0x84, 0x41, 0x06, 0x00, 0x00, 0x00, 0x04, 0x84, 0x42, 0x08, 0x00, 0x00, 0x00, 0x04, 0x88,
0x42, 0x10, 0x00, 0x00, 0x00, 0x04, 0x88, 0x84, 0x20, 0x00, 0x00, 0x00, 0x04, 0x08, 0x84, 0x40,
0x00, 0x00, 0x00, 0x04, 0x08, 0x84, 0x80, 0x00, 0x00, 0x00, 0x04, 0x11, 0x03, 0x04, 0x00, 0x00,
0x00, 0x04, 0x11, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x11, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
0x22, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00,
0x04, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Shoot icon bitmap
const unsigned char shootBitmap [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0b, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x80, 0x80, 0x00, 0x00, 0x00, 0x40, 0x11, 0x81,
0x80, 0x00, 0x00, 0x00, 0x70, 0x21, 0x83, 0x00, 0x00, 0x00, 0x00, 0x50, 0x21, 0x87, 0x00, 0x00,
0x00, 0x00, 0x48, 0x61, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x44, 0x41, 0xb3, 0x00, 0x00, 0x08, 0x00,
0x42, 0x41, 0xc6, 0x00, 0x00, 0x0e, 0x00, 0x43, 0x81, 0x86, 0x00, 0x00, 0x07, 0xc0, 0x41, 0x81,
0x0e, 0x00, 0x00, 0x01, 0x38, 0x40, 0x00, 0x0e, 0x00, 0x30, 0x00, 0x87, 0x40, 0x00, 0x0c, 0x1f,
0xe0, 0x00, 0x40, 0xc0, 0x00, 0x1f, 0xf1, 0x80, 0x00, 0x60, 0x00, 0x00, 0x1e, 0x03, 0x80, 0x00,
0x30, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x04, 0x00,
x00, 0x00, 0x1c, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
0xf0, 0x00, 0x00, 0x60, 0x00, 0x00, 0x01, 0xf0, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x00, 0x1f, 0x80,
0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x00, 0x07,
0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x30, 0x00, 0x00,
0x00, 0x7c, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x70,
0x00, 0x07, 0xff, 0xc0, 0x00, 0x00, 0x3c, 0x00, 0x1f, 0xff, 0xc1, 0x80, 0x0c, 0x1e, 0x00, 0x3f,
0xff, 0xe1, 0xc2, 0x0e, 0x0e, 0x00, 0x00, 0x00, 0x23, 0xe7, 0x8f, 0xc7, 0x00, 0x00, 0x00, 0x27,
0xf7, 0xc7, 0xf3, 0x80, 0x00, 0x00, 0x27, 0x3e, 0x76, 0x3d, 0xc0, 0x00, 0x00, 0x2f, 0x1c, 0x3e,
0x1f, 0xe0, 0x00, 0x00, 0x2e, 0x0c, 0x1e, 0x03, 0xf0, 0x00, 0x00, 0x3c, 0x00, 0x06, 0x00, 0x78,
0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00,
0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Run icon bitmap
const unsigned char runBitmap [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
0x08, 0x04, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x10, 0x02, 0x00, 0x3f, 0xfe, 0x31, 0x80, 0x10, 0x02,
0x00, 0x00, 0x00, 0x40, 0x40, 0x10, 0x02, 0x00, 0x00, 0x00, 0x80, 0x20, 0x10, 0x02, 0x00, 0x00,
0x03, 0x00, 0x10, 0x10, 0x02, 0x00, 0x00, 0x04, 0x00, 0x0c, 0x10, 0x02, 0x00, 0x00, 0x08, 0x0c,
0x02, 0x08, 0x04, 0x00, 0x0f, 0x10, 0x12, 0x01, 0x84, 0x08, 0x00, 0x00, 0x20, 0x61, 0x00, 0x43,
0xf0, 0x00, 0x00, 0x40, 0x80, 0x80, 0x20, 0x00, 0x3c, 0x00, 0x41, 0x00, 0x40, 0x10, 0x00, 0x42,
0x00, 0x42, 0x00, 0x40, 0x08, 0x01, 0x82, 0x00, 0x3c, 0x00, 0x80, 0x04, 0x02, 0x02, 0x00, 0x00,
0x03, 0x00, 0x02, 0x04, 0x04, 0x00, 0x00, 0x04, 0x00, 0x01, 0x08, 0x08, 0x00, 0x00, 0x08, 0x00,
0x00, 0xf0, 0x10, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x3f, 0xfc, 0x20, 0x01, 0xe0, 0x00,
0x40, 0x00, 0x00, 0x40, 0x02, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x04, 0x0c, 0x01, 0x00, 0x00,
0x00, 0x80, 0x08, 0x02, 0x06, 0x00, 0x00, 0x01, 0x00, 0x10, 0x01, 0x88, 0x00, 0x00, 0x01, 0x00,
0x20, 0x00, 0x70, 0x00, 0x03, 0xf9, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x62, 0x00, 0x00, 0x00,
0x00, 0x02, 0x04, 0x81, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x80, 0x80, 0x00, 0x00, 0x00, 0x02,
0x04, 0x80, 0x40, 0x00, 0x00, 0x00, 0x04, 0x08, 0x80, 0x20, 0x00, 0x00, 0x00, 0x04, 0x08, 0x40,
0x20, 0x00, 0x00, 0x00, 0x04, 0x08, 0x20, 0x20, 0x00, 0x00, 0x00, 0x08, 0x08, 0x20, 0x20, 0x00,
0x00, 0x00, 0x30, 0x10, 0x20, 0x40, 0x00, 0x00, 0x00, 0x40, 0x10, 0x40, 0x40, 0x00, 0x00, 0x01,
0x80, 0x20, 0x80, 0x80, 0x00, 0x00, 0x06, 0x00, 0x60, 0x81, 0x00, 0x00, 0x00, 0x08, 0x00, 0xc1,
0x02, 0x00, 0x00, 0x00, 0x10, 0x01, 0x01, 0x02, 0x00, 0x00, 0x00, 0x20, 0x06, 0x02, 0x04, 0x00,
0x00, 0x00, 0x40, 0x18, 0x04, 0x08, 0x00, 0x00, 0x00, 0x40, 0x20, 0x04, 0x10, 0x00, 0x00, 0x00,
0x40, 0xc0, 0x04, 0x10, 0x00, 0x00, 0x00, 0x43, 0x00, 0x04, 0x20, 0x00, 0x00, 0x00, 0x3c, 0x00,
0x03, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Left icon bitmap
const unsigned char leftBitmap [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x19, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc1, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x81, 0x80, 0x00,
0x00, 0x00, 0x00, 0x03, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 0x80, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x01, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x18, 0x01, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x30,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xc0, 0x00, 0x00,
0x00, 0x00, 0x02, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x18,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x60, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x80,
0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xe0, 0x00, 0x00,
0x00, 0x00, 0x02, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x1c, 0x01, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x0e, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00,
0x07, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x03, 0x81, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc1,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe1, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00, 0x39, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x80, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Right icon bitmap
const unsigned char rightBitmap [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x9c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x83, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x81,
0xc0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x70, 0x00,
0x7f, 0xff, 0xff, 0xff, 0x80, 0x38, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x40, 0x00,
0x00, 0x00, 0x00, 0x0e, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x40, 0x00, 0x00, 0x00,
0x00, 0x03, 0x80, 0x40, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0xe0, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x40,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x40, 0x00, 0x00,
0x00, 0x00, 0x00, 0x06, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x40, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x40, 0x00,
0x00, 0x00, 0x00, 0x01, 0x80, 0x40, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x00,
0x00, 0x06, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x7f, 0xff, 0xff, 0xff, 0x80, 0x18,
0x00, 0x7f, 0xff, 0xff, 0xff, 0x80, 0x30, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x60, 0x00, 0x00,
0x00, 0x00, 0x01, 0x80, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x81, 0x80, 0x00, 0x00, 0x00, 0x00,
0x01, 0x83, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8c,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Up icon bitmap
const unsigned char upBitmap [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc3,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x81, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xc0, 0x00,
0x00, 0x00, 0x00, 0x0e, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x30, 0x00, 0x00, 0x00,
0x00, 0x38, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0xe0,
0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x01,
0x80, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x60, 0x00,
0x00, 0x1c, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x70,
0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0xc0, 0x00, 0x00,
0x00, 0x03, 0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x01, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0xc0, 0x07, 0xff, 0x80, 0x00, 0x03, 0xff, 0xe0, 0x07, 0xff, 0x80, 0x00, 0x03, 0xff, 0xe0, 0x00,
0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80,
0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03,
0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00,
0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00,
0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00,
0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00,
0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80,
0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03,
0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00,
0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00,
0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00,
0x03, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Down icon bitmap
const unsigned char downBitmap [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x01, 0x00, 0x00, 0x07, 0xff, 0xc0, 0x00, 0x01, 0xff, 0xe0,
0x07, 0xff, 0xc0, 0x00, 0x01, 0xff, 0xe0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x80,
0x00, 0x00, 0x00, 0x01, 0xc0, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x03, 0x80, 0x00, 0x60, 0x00, 0x00,
0x00, 0x07, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1c,
0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00,
0x03, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x01, 0x80, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x00, 0xc0,
0x00, 0x03, 0x80, 0x00, 0x00, 0x00, 0x60, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x0e,
0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x38, 0x00, 0x00,
0x00, 0x00, 0x06, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00,
0x01, 0x81, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
Each of the custom graphics used for visual feedback is defined here as bitmap images, which is the required format to display on the OLED.
Graphics include a microphone as well as a unique icons representing each of the voice command actions.
3. Define a display object:
// Adafruit OLED object
int SCREEN_WIDTH = 128;
int SCREEN_HEIGHT = 64;
int SCREEN_ADDRESS = 0x3D;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire);
An instance of the Adafruit_SSD1306
object is created by passing in the OLED screen width and screen height. This object includes methods for manipulating what is displayed on the OLED.
4. Add the following animation settings:
// Listening animation settings
int centerX = SCREEN_WIDTH / 2;
int centerY = SCREEN_HEIGHT / 2;
int maxRadius = 31;
int minRadius = 23;
int animationFrames = 12;
int animationFrame = 0;
Parameters that control the listening graphic animation are defined here. The animation includes a pulsing ring, the speed and size of which are configured by these variables.
5. Add the following command icon settings:
// Voice command icon settings
int iconSize = 56;
int iconIndex = -1;
bool iconDisplayed = false;
unsigned long iconStartMillis;
const unsigned long iconDuration = 1500;
Parameters that control the command graphic icons are defined here. These variables track which icon is to be shown and the display duration (1.5 sec).
6. Update the VoiceCommand
definition to the following:
struct VoiceCommand {
String name;
int key;
unsigned long startMillis;
unsigned long period;
bool isPressed;
const unsigned char* bitmap;
};
7. Then, update the voiceCommands
to the following:
// Game voice commands
const int numCommands = 7;
VoiceCommand voiceCommands[numCommands] = {
{"Jump", jumpCommandKey, 0, 500, false, jumpBitmap}, // Jump
{"Run", runCommandKey, 0, 500, false, runBitmap}, // Run
{"Shoot", shootCommandKey, 0, 250, false, shootBitmap}, // Shoot
{"Up", upCommandKey, 0, 1000, false, upBitmap}, // Up
{"Down", downComandKey, 0, 1000, false, downBitmap}, // Down
{"Left", leftCommandKey, 0, 1000, false, leftBitmap}, // Left
{"Right", rightCommandKey, 0, 1000, false, rightBitmap} // Right
};
With these updates, each VoiceCommand
now stores its unique bitmap
image graphic alongside the other data that defines the command.
8. Add the following inside the script's setup()
function:
// Initialize oled display
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
The OLED display is initialized and then cleared prior to rendering graphics.
9. Within the processVoice()
function, below voiceCommands[index].startMillis = millis();
add the following line:
iconIndex = index;
With this update, every time a voice command is detected, its index value is stored in the iconIndex
variable.
10. Define a processDisplay()
function at the end of the sketch:
void processDisplay() {
// Display command icon if active
if (iconIndex > -1) {
if (iconDisplayed == false) {
// Display the selected icon
display.clearDisplay();
display.drawBitmap(
centerX - iconSize/2,
centerY - iconSize/2,
voiceCommands[iconIndex].bitmap,
iconSize,
iconSize,
WHITE
);
iconDisplayed = true;
iconStartMillis = millis();
} else {
unsigned long currentMillis = millis();
if (currentMillis - iconStartMillis >= iconDuration) {
iconIndex = -1;
iconDisplayed = false;
}
}
}
// Display listening animation
else {
// Calculate circle radius
float animationProgress = float(animationFrame) / float(animationFrames - 1);
float animationPhase = animationProgress * 2.0 * PI;
float pulsatingFactor = (cos(animationPhase) + 1.0) / 2.0;
int currentRadius = minRadius + int((maxRadius - minRadius) * pulsatingFactor);
// Display mic and circle
display.clearDisplay();
display.drawBitmap(
centerX - micBitmapSize/2,
centerY - micBitmapSize/2,
micBitmap,
micBitmapSize,
micBitmapSize,
WHITE
);
display.drawCircle(centerX, centerY, currentRadius, SSD1306_WHITE);
// Update frame count
animationFrame = (animationFrame + 1) % animationFrames;
}
// Update display
display.display();
}
11. Then call this function within the sketch's loop()
function:
// Process display
processDisplay();
The processDisplay()
function is responsible for updating the display to provide visual feedback to the user and is executed within each iteration of the loop()
function.
When an active voice command is detected (iconIndex
having a value greater than 0), the function manages the display of its associated icon.
If iconDisplayed
is false, indicating that the icon hasn't been displayed yet, the function clears the display and uses the displayBitmap()
method to show the associated bitmap.
Additionally, it sets iconDisplayed
to true and records the current time in iconStartMillis
.
If the icon is already being displayed, the code checks if it has exceeded its iconDuration
value, and if so, iconIndex
is reset to -1, and iconDisplayed
is set back to false.
When there's no active voice command (iconIndex
is -1), the function displays a listening animation. On each iteration, the radius of a pulsating ring is calculated, and both the ring and microphone bitmap are displayed.
With the above steps complete, the controller prototype is fully functional and ready for use. Users can upload the Arduino sketch to the Arduino Micro, and test each of the features that have been implemented.
The buttons, joystick, and voice commands can be pressed, moved, and spoken to transmit their associated keystrokes to the connected computer.
This can be witnessed by opening a text editor on the computer to see each keystroke printed out, or by opening a video game to see corresponding game actions get triggered.
The OLED display shows a listening animation as it waits for a voice command from the user, then updates with a custom icon when one is detected.
Controller AssemblyWith the controller prototype tested and complete, the hardware can be transferred from the breadboard and assembled into its finished design.
1. Begin by 3D printing the controller body:
3D model files (.stl) are provided in this project's Custom Parts and Enclosures section. This includes the front and back plates of the controller body.
The version of the controller shown in this project is printed using PLA, but other print materials can be used as well.
2: Next, construct supports for the controller's buttons:
To be mounted within the controller body, the two sets of buttons must first be attached to protoboards, which secure them in place and expose their pins for connections to the microcontroller.
Cut squares of protoboard to fit the controller's four and two-button configurations, then solder the buttons to the board. Use a drill to create a center hole that can fit a M3.5 size screw.
3. Remove the header pins from the Arduino Micro, analog joystick, and Elechouse Voice Recognition module.
This step is required to provide clearance space to fit these components within the controller body and can be accomplished by desoldering each pin and pulling it from the board's through hole.
4. Mount each of the components within the controller body:
Align each component's mounting holes with its corresponding mount in the controller body and then attach them with machine screws.
To the front plate, attach the OLED display and the analog joystick with M2.5x8 screws, and both button supports with M3.5x5 screws.
To the back plate, attach the Arduino Micro with M1x6 screws and the voice recognition module with M2.5x5 screws.
4. Solder circuit connections between components:
Following the circuit diagram, solder the required connections between each of the components using jumper wires. Using 30AWG wire is recommended due to its thinness and flexibility, allowing it to fit within the controller body.
After soldering is complete, electrical tape can be used to cover any exposed circuitry on each of the components to prevent any shorts from occurring.
5. Attach both halves of the controller body:
Align the mounting holes located along the edges of the front and back plates, and attach them using M2.5x8 screws. Ensure the USB and audio ports are properly exposed through the controller body.
6. Plug the microphone and USB cable into their controller ports:
With these steps complete, the controller is fully assembled and ready for use. Connect the controller to any computer via a USB cable to power it on and begin playing games.
Future DevelopmentThis project has demonstrated the feasibility of a handheld controller that offers on-device voice recognition to assist gamers with disabilities. Future upgrades to the controller may include:
- AI Voice Integration: AI and machine learning technologies can improve voice recognition accuracy, eliminate the need for voice training, and recognize hundreds of spoken words. Microcontrollers with AI/ML processors, or services like the Arduino Speech Recognition Engine could be used to implement this advanced form of on-device voice recognition.
- Bluetooth Connectivity: The ability for the controller to connect to the computer over Bluetooth, instead of a USB cable, would increase the freedom of movement and set up convenience for disabled games. Arduino boards with built-in Bluetooth, such as the Nano ESP32, support this feature. An internal battery would be added to the controller as well.
- InputMapping Screen: An interactive menu on the controller's OLED display could provide a better experience for customizing input mappings, instead of requiring gamers to edit the controller's code. This feature would let users view and edit their mapping settings using the controller's inputs, and instantly apply them after saving their changes.
Comments