$50 Analog Joystick (HOTAS) With Haptic Feedback for Flight Sim




Introduction: $50 Analog Joystick (HOTAS) With Haptic Feedback for Flight Sim

About: IOT, HomeKit, Arduino, Raspberry Pi, Robotics.

The aim is to create a fully functioning USB HOTAS* joystick from low cost electronics parts.

I bought Elite Dangerous when it was released for the Mac and realised very quickly I was going to have the floor wiped with me if I didn't have a decent joystick, the problem is a really good joystick costs 100's of dollars or $450 in the case of this one: http://arstechnica.com/gadgets/2014/09/flying-in-s... . Its very difficult for me to tell my wife should doesn't need another pair of shoes if I order a $450 joystick for playing a "space game".


Fully USB powered
Mac / PC / Linux Compatible

Programable Micro Controller
Analog X/Y Axis primary joystick
Analog X/Y Axis sub controller
Analog Throttle capable of 1024 acceleration points and boost
Weapon Arming toggle switch
2 x Analog Trimming Knobs
4 x Programable Momentary Buttons
4 Vibration motors

*HOTAS Stands for Hands on Throttle and Stick, so this is not a 'typical' HOTAS Joystick, by some definitions it would have to have a DPAD on the joystick controller, where my DPAD is controlled by the thumb of the throttle hand. But definitions aside this is an extremely capable game controller and infinitely customisable thanks to the on board programable micro controller.

Step 1: Parts List

$10 - Arduino Leonardo

$3 - Old Wii Nunchuck

$19 - Joystick with potentiometers - type used for CCTV systems or wheelchairs

$3 - ABS Enclosure

$2.5 Flip up weapon toggle switch

$4 - Sliding Potentiometer

$0.5 - 2 x100K Potentiometers

$0.5 - a few Dupont cable connectors

$2 - 4x mini vibration units

$2 - L289n Motor driver

$2 - 4x minibuttons

$2 - 'Carbon fibre' vinyl wrap

The hardware is under $50 including postage to Australia.

It should be said I'm not picky about brands, the only genuine parts here are the Dupont cables, and I'm not even sure about them, this is how I managed to keep the costs down.

Step 2: The Brain

The $10 Arduino Leonardo is the brain of the joystick - it has a few advantages over to more commonly known UNO model. One of which is that it can act directly as a USB mouse or Keyboard. Example sketches are included with the Arduino software that allow you to control a mouse x & y access and also a 'scroll wheel' (which I connect to the throttle).

So out of the box the Leonardo can control 3 joystick axis* + give you access to the entire suite of keyboard commands, pretty good for 10 bucks.


I also used Ordered Bytes amazing Mac Controllermate software to read the USB events and translate to a virtual keyboard joystick. this made it so much easier to set up centre points etc and calibrate the joystick I would not really enjoy having to do this in the Arduino code - not 100% necessary but life is short. I'm not sure of a PC or Linux equivalent to Controllermate, but there will be something.
You could just build this, load the code in and it would work, but Controller mate makes it really easy to do things like adjusting the left right balance, or adding an automatic boost trigger to the accelerator.

* There is a firmware mod for Arduino UNO's out there that allows control for more than 3 axis - but I have not modded the firmware on this instructable.

Step 3: The Main Joystick

I purchased an industrial grade joystick (JH-D202X) for US $18.57 from ebay.
They have a few variations and they seem really solid.

If you get a different joystick, make sure you get one with potentiometers not microswitches - there are a few models available, they also have models with buttons on top.

There are 3 pins on both the x and y axes, the side pins on each axes connect to 5v and GND on the Arduino.

The centre pins connect to the Analog pin you will use to read the data on the Arduino.

x axis rolls the craft left and right

yaxis pitches the crafts nose up and down

Step 4: The Sub Controller (mini Joystick)

To make this I butchered a cheap Wii Nunchuck - basically opened it up and removed the joystick.

It's basically the same (2 potentiometer joystick) as the main joystick, but smaller- I soldered some dupont cables onto the 6 fiddly connectors, and connected the side pins to 5V and GND - and then the middle pins to the appropriate Arduino pin.

It's controlled with your left thumb or right thumb for lefties:

When flying it gives you left and right YAW and Vertical up /down thrust

When in an interface it controls up, down, left, right

You can also configure it to behave differently when in landing mode << just do this in the controller options in game.

x left presses the a key.
x right presses the d key
y up presses the r key
y down presses the f key

NB the YAW flight control provided here is not analog, its just on/off - this works great for me. Because of the limited axis supported by the Arduino, this is our only option without modding the Arduino firmware.

Step 5: The Throttle

For a throttle I bought a $4 sliding potentiometer from eBay, this is the kind of thing you see on a mixing desk. Again this is another potentiometer, so connect it to another analog in on the Arduino. $4 is a bit much for this, I'm sure you could get a cheaper one.

Z axis increases and decreases the velocity

+ Pushing it to the extreme top causes the TAB key to be pressed and a boost to trigger << this is genius, once you try this you can't live without it.

There are six pins (in order):

OTA > ignore this pin
VCC> ignore this pin
GND> ignore this pin

OTB > to Arduino Analog Pin
VCC > to 5v
GND > to Earth

Step 6: The Weapons

I needed a weapon arming system (to deploy hard points) , so one of these low cost "missile switches" seemed ideal.

But I also needed to have the ability to trigger individual weapons on common, so I purchased 4 red momentary pushbuttons, in the image above I have started soldering the cables.

Step 7: Haptic Feedback

The Arduino Leonardo has a lot of pins - so it would be foolish resist the temptation to add vibration units to the joystick, these ones even have self adhesives backs.

Basically you connect the the positive wire of the vibration units direct to the Arduino digital pins then when you pass a current though the Dupont compatible pin - it will start the shakes up. You can pulse and trigger the units as you want.

One thing you can't do yet is to respond to events in the game - Like being hit or exploding. - you can only respond to your own input events. This still leaves room for control feedback, events I added are:

1) Micro pulse every time you hit a button

2) Micro pulse at the correct side when main joystick hits an extremity

3) Two second big buzz fading out when you press velocity boost

4) One send stuttering buzz when you deploy hard points or the landing gear

Step 8: The Enclosure

In the attached image I have mounted the Wii Nunchuck joystick, the 2 potentiometers and drilled the holes and made the slot for the sliding potentiometer.

The idea is you use your left hand for the sliding pot and wii joystick and your right thumb for the serious joystick.

It can be used on a desk, or in your hand like a giant controller.

Step 9: Adding the Controllers

In this first image I have mounted the sliding pot, the weapon arming toggle switch and the main joystick, the circular surround and screws are provided with the joystick its easy to mount and hole doesn't need to be exact.

Behind the back I have mounted the 4 custom buttons.

Step 10: Check Inside

Here you can see I have mounted the Arduino and connected up the potentiometers.

I have added an earth and live rail on the bottom right to try and keep it tidy.

Whats still missing in this image- 4 programable buttons & vibration motors (also the weapon arming is not connected up)

Step 11: Step 10 Final Touches

I have curved the box edges with my Dremel and use a small amount of polymorph plastic to fill any holes.

I also have added grooves for my fingers to wrap around if I am picking up the joystick and using it like a handheld controller.

The next step was to remove the control parts and wrap the whole thing in half adhesive "carbon fibre" and reinstall the controller parts. I used a hair dryer to make the self adhesive plastic mould to the enclosure better.

Step 12: Step 9 - the Code - Paste Into the Arduino IDE to See What Is Happening



Controls the mouse from a joystick on an Arduino Leonardo, Micro or Due. Uses a pushbutton to turn on and off mouse control, and a second pushbutton to click the left mouse button

Hardware: * 2-axis joystick connected to pins A0 and A1 * pushbuttons connected to pin D2 and D3

The mouse movement is always relative. This sketch reads two analog inputs that range from 0 to 1023 (or less on either end) and translates them into ranges of -6 to 6. The sketch assumes that the joystick resting values are around the middle of the range, but that they vary within a threshold.

WARNING: When you use the Mouse.move() command, the Arduino takes over your mouse! Make sure you have control before you use the command. This sketch includes a pushbutton to toggle the mouse control state, so you can turn on and off mouse control.

includes 15 Sept 2011 updated 28 Mar 2012 by Tom Igoe

this code is in the public domain


/ set pin numbers for switch, joystick axes, and LED: //const int switchPin = 2; // switch to turn on and off mouse control const int mouseButton = 3; // input pin for the mouse pushButton const int xAxis = A0; // joystick X axis const int yAxis = A1; // joystick Y axis const int zAxis = A2; // joystick z axis (speed)

// parameters for reading the joystick: int range = 127; // output range of X or Y movement int responseDelay = 5; // re sponse delay of the mouse, in ms int center = range / 2; // resting position value int weaponArm = 10;//pin for weapon arm int vibratorOne = 9;//pin for vibrator int trig1 = 12;//pin fire1 int trig2 = 11;//pin fire2

int shakeCountX1 = 1; // number of cycles for quickshake int shakeCountX2 = 1; // number of cycles for quickshake int shakeCountY1 = 1; // number of cycles for quickshake int shakeCountY2 = 1; // number of cycles for quickshake boolean mouseIsActive = false; // whether or not to control the mouse boolean toggleState; // hold current state of switch boolean lastToggleState; // hold last state to sense when switch is changed int trig1State = 0; int trig2State = 0; long toggleTimer = millis(); // // debounce timer

void setup() {

// pinMode(trig1, INPUT_PULLUP); // pinMode(trig2, INPUT_PULLUP); pinMode(weaponArm, INPUT_PULLUP); pinMode(vibratorOne, OUTPUT);

Mouse.begin(); }

void loop() { delay(10);

mouseIsActive = !mouseIsActive;

int xReading = readAxis(xAxis); int yReading = readAxis(yAxis); int sReading = readAxis(zAxis); int DVertReading = readAxis(A3); int DHorizReading = readAxis(A4); Mouse.move(xReading, yReading, sReading); // temporailty disabled until joystick arrives // Mouse.move(0, 0, sReading); //delete this one when joystick comes

// delay(100); trig1State = digitalRead(trig1); trig2State = digitalRead(trig2);


if (DVertReading > 20) { Keyboard.write('r'); //SAME KEY TO HIDE AND SHOW }

if (DVertReading < -30) { Keyboard.write('f'); //SAME KEY TO HIDE AND SHOW }

if (DHorizReading > 20) { Keyboard.write('a'); //SAME KEY TO HIDE AND SHOW } if (DHorizReading < -30) { Keyboard.write('d'); //SAME KEY TO HIDE AND SHOW }


if (xReading > 61) { int sens; sens = quickShakeX1();} else{ shakeCountX1 = 1; }

if (xReading < -61) { int sens; sens = quickShakeX2();} else{ shakeCountX2 = 1; }

if (yReading > 61) { int sens; sens = quickShakeY1();} else{ shakeCountY1 = 1; }

if (yReading < -61) { int sens; sens = quickShakeY2();} else{ shakeCountY2 = 1; }


toggleState = digitalRead(weaponArm);

if (millis() - toggleTimer > 100){ // debounce switch 100ms timer if (toggleState && !lastToggleState) { // if switch is on but was previously off lastToggleState = true; // switch is now on toggleTimer = millis(); // reset timer Keyboard.write('u'); }

if (!toggleState && lastToggleState){ // if switch is off but was previously on lastToggleState = false; // switch is now off toggleTimer = millis(); // reset timer Keyboard.write('u'); //SAME KEY TO HIDE AND SHOW } }

// read the mouse button and click or not click: // if the mouse button is pressed: if (digitalRead(mouseButton) == HIGH) { // if the mouse is not pressed, press it: if (!Mouse.isPressed(MOUSE_LEFT)) { Mouse.press(MOUSE_LEFT); } } // else the mouse button is not pressed: else { // if the mouse is pressed, release it: if (Mouse.isPressed(MOUSE_LEFT)) { Mouse.release(MOUSE_LEFT); } }

delay(responseDelay); }

/* reads an axis (0 or 1 for x or y) and scales the analog input range to a range from 0 to */

int readAxis(int thisAxis) { // read the analog input: int reading = analogRead(thisAxis);

// map the reading from the analog input range to the output range: reading = map(reading, 0, 1023, 0, range);

// if the output reading is outside from the // rest position threshold, use it: int distance = reading - center;

// return the distance for this axis: return distance; }

int quickShakeX1(){ if (shakeCountX1 > 0) { digitalWrite(13, HIGH);// Show led to see if motor would run digitalWrite(vibratorOne, HIGH); // turn the LED on (HIGH is the voltage level) delay (200); digitalWrite(13, LOW);// Show led to see if motor would run digitalWrite(vibratorOne, LOW); // turn the LED on (HIGH is the voltage level) shakeCountX1 = shakeCountX1 - 1; }


int quickShakeX2(){ if (shakeCountX2 > 0) { digitalWrite(13, HIGH);// Show led to see if motor would run digitalWrite(vibratorOne, HIGH); // turn the LED on (HIGH is the voltage level) delay (200); digitalWrite(13, LOW);// Show led to see if motor would run digitalWrite(vibratorOne, LOW); // turn the LED on (HIGH is the voltage level) shakeCountX2 = shakeCountX2 - 1; }


int quickShakeY1(){ if (shakeCountY1 > 0) { digitalWrite(13, HIGH);// Show led to see if motor would run digitalWrite(vibratorOne, HIGH); // turn the LED on (HIGH is the voltage level) delay (200); digitalWrite(13, LOW);// Show led to see if motor would run digitalWrite(vibratorOne, LOW); // turn the LED on (HIGH is the voltage level) shakeCountY1 = shakeCountY1 - 1; }


int quickShakeY2(){ if (shakeCountY2 > 0) { digitalWrite(13, HIGH);// Show led to see if motor would run digitalWrite(vibratorOne, HIGH); // turn the LED on (HIGH is the voltage level) delay (200); digitalWrite(13, LOW);// Show led to see if motor would run digitalWrite(vibratorOne, LOW); // turn the LED on (HIGH is the voltage level) shakeCountY2 = shakeCountY2 - 1; }


Step 13: Configuring the Axis and Throttle (not Essential)

In ControllerMate I created a Virtual Joystick (Toms Home Joystick)

You can see in the image how I am connecting:

Arduino Mouse output > Axis Calibration Module > Virtual Joystick

Elite dangerous thinks the Virtual Joystick is a real joystick

Notice the branch at then bottom of the throttle - this is picking up when the throttle is maxed out and presses the TAB key to cause the boost.

Step 14: Configuring the Sub Joystick (not Essential)

You can skip this step if you don't have ControllerMate.

In controller mate it detects the key presses from the Arduino code and translates them ........to the same keypresses. The reason I bother with controllermate is that we can add the dwell function between the two, this softens the output to Elite Dangerous and leads to a smoother ride.

Be the First to Share


    • Jewelry Challenge

      Jewelry Challenge
    • Anything Goes Contest 2021

      Anything Goes Contest 2021
    • Fix It Speed Challenge

      Fix It Speed Challenge



    1 year ago

    Well done!