Tuesday, January 24, 2023

I made a joystick. i.e. Why pay $X for something you can spend $(X+Y) building ¯\_(ツ)_/¯

I Made A Joystick.

    I was playing Microsoft Flight Simulator - gorgeous graphics! - and amidst the splendor of... someplace I've never been, I found myself wanting more physical controls. I have a nice joystick - A VKB Gladiator. It even has a little throttle. That's the only analog control other than the stick though. It's far from bad, but I just felt like trying more...


    The first place I checked was amazon. The Thrustmaster TCA "Airbus" controller caught my eye. It was expandable to 4 levers, and there was even a sale. So I bought it...
    Then I thought about checking Reddit. it's not a bad controller, but there was a lot of love for the pricier fare. Of course, I then stumble onto threads of homemade joysticks. "Yeah... I can do that..." I thought - and off I dove into Amazon again, canceling my order, and getting a project box, various switches, wires, and an Arduino Leonardo.


Are you board?

    It should be noted that not every Arduino board can work in a joystick project (at least, not normally to my knowledge). The Leonardo can, and there are a few libraries that will work with it. 

    That takes care of me telling the computer what to do - "button 5 is pressed", or "set that much throttle". Now I need to figure out how to put together the physical switches, potentiometers etc and read them in.


Building!

    I started with the project box - cutting holes for the switches - and a couple radio-control replacement sticks. These are modular joysticks for use in radio-control transmitters - like the type you'd control a model airplane with.

    This simplified the project - I didn't have to develop a gimbal. This has it all built in. It's small - a thumb stick - but this is fine for at least version 1 right?
    NB: looks like there are nicer stick modules about too. "FrSky" has an all-aluminum hall sensor model. Version 2? 😀



    I used sticker paper with a cutout to get the holes right before applying it to the inside of the project box, and using my scroll saw to cut the larger holes and slider-hole. The smaller holes were simply drilled. The texture of the box is the same as my last headset - Rhino-liner.


    
    Measure twice, and cut once. With the holes cut, everything fit quite nicely. Rhinoliner did get pretty scratched while bolting in the buttons so I make another pass with a touch of bronze.



    The underside of the panel looked like it provided plenty of room - Piece of cake right? (The cake is a lie!)
    Meanwhile, I'm messing with the Arduino code, trying to see what it will take to read the values correctly.



    I've got 16 buttons (6 up-down toggle + 4 regular) - I don't think I had enough pins - so the alternative, is to use 8 pins in a 4x4 grid. Loop through one row of pins sending a signal, then check each pin in the column for a response. If it's there, the button is on. Throw in a diode, and you can press multiple buttons without issue.


I ain't 'fraid of no ghosting...





    Turns out you cannot assume an input pin is going to be high or low - it should be pulled to that point - or else you can expect some garbage. Arduino has an input mode for this - "INPUT_PULLUP".
    
    From the grid above, pins 1-4 are read. Pins A-D are scanned.

    So I'm taking "HIGH" as normal (not pressed). If I press a button that closes a switch - say at "C-2" in the diagram, then as I scan the pins - set "C" to low, then read 1-4 - pin 2 will register low, button press. When done with scanning for "C", set "C" again to high.

    The diode setup prevents ghosting - you can smush a bunch of buttons and only those buttons show up as pressed. Without the diode, singular button presses are fine - but if I were to press A1, A2 and B1 - then B2 will show up as pressed as well.


So... many... wires...

Gaaahhh!!


    
    The most annoying bit of this was soldering all the wires in place. The board needed several wires to act as rows and columns. 4 wires for reading, 4 for the scan, and 32 for 16 buttons. Wires tend to come joined - Joined wires appear to put a lot of stress on the shorter wire. I had to resolder a few that pulled away from the board.


    Soldering the wires to the buttons after that was no picnic either!



    
    I thought I had enough space - really. 



    I had to get creative, bending the headers on the Arduino board a bit. Heatshrink was getting in the way, so I went with masking tape and tucked those wires above one of the stick modules. Not the best, but it will do.



    Yay - we have a pulse!



It doesn't look much different, but it's got a brain now! I've tried it in MS Flight Simulator, and Superflight. I still need to try it in Kerbal Space Program 😀.


Code:

#include "Joystick.h"

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID, JOYSTICK_TYPE_JOYSTICK, 16, 0,
   true,  true,  true, false, false, false,  true, true, false, false, false);
//    X,     Y,     Z,    RX,    RY,    RZ,  rudr, throt, accel, brake, steer);


#define JOYAX1_PIN  A1 // Arduino pin connected to VRX pin
#define JOYAX2_PIN  A2 // Arduino pin connected to VRY pin
#define JOYAX3_PIN  A4 // Arduino pin connected to VRY pin
#define JOYAX4_PIN  A3 // Arduino pin connected to VRY pin
#define JOYAX5_PIN  A0 // Arduino pin connected to VRY pin
#define HIGHPIN 13
#define CHKPIN 12

#define buttchanA 2
#define buttchanB 3
#define buttchanC 4
#define buttchanD 5

#define buttreadA 8
#define buttreadB 9
#define buttreadC 7
#define buttreadD 6

int ButtonState[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

int reRange(int inval, int inmin, int inmax, int neg){
  int range=inmax-inmin;
  float oldval=inval;
  int newval=(((oldval-inmin)*1023)/range);
  newval=min(newval,1023);
  newval=max(newval,0);
  if (neg<0) {
    newval=1023-newval;
  }
  return int(newval);
}

void setup() {
  //Serial.begin(9600);
  pinMode(JOYAX1_PIN, INPUT_PULLUP);
  pinMode(JOYAX2_PIN, INPUT_PULLUP);
  pinMode(JOYAX3_PIN, INPUT_PULLUP);
  pinMode(JOYAX4_PIN, INPUT_PULLUP);
  pinMode(JOYAX5_PIN, INPUT_PULLUP);
 
  pinMode(HIGHPIN, OUTPUT);
  digitalWrite(HIGHPIN, HIGH);
  pinMode(buttchanA, OUTPUT);
  pinMode(buttchanB, OUTPUT);
  pinMode(buttchanC, OUTPUT);
  pinMode(buttchanD, OUTPUT);
  digitalWrite(buttchanA, HIGH);
  digitalWrite(buttchanB, HIGH);
  digitalWrite(buttchanC, HIGH);
  digitalWrite(buttchanD, HIGH);
  pinMode(buttreadA, INPUT_PULLUP);
  pinMode(buttreadB, INPUT_PULLUP);
  pinMode(buttreadC, INPUT_PULLUP);
  pinMode(buttreadD, INPUT_PULLUP);
  pinMode(CHKPIN, INPUT);
  Joystick.begin();
}

void loop() {
  int joy_ax1_val = analogRead(JOYAX1_PIN); // X axis, right stick regx
  int joy_ax2_val = analogRead(JOYAX2_PIN); // y axis, right stick regy
  int joy_ax3_val = analogRead(JOYAX3_PIN); // X axis, right stick rudder
  int joy_ax4_val = analogRead(JOYAX4_PIN); // Y axis, right stick throttle
  int joy_ax5_val = analogRead(JOYAX5_PIN); // Z axis, slider for trim              

  int pinW;
  int pinR;
  for (int idxW = 0; idxW < 4; idxW++){
    if (idxW == 0) {pinW=buttchanA;}
    if (idxW == 1) {pinW=buttchanB;}
    if (idxW == 2) {pinW=buttchanC;}
    if (idxW == 3) {pinW=buttchanD;}
    digitalWrite(pinW, LOW);
    for (int idxR = 0; idxR < 4; idxR++){
      if (idxR == 0) {pinR=buttreadA;}
      if (idxR == 1) {pinR=buttreadB;}
      if (idxR == 2) {pinR=buttreadC;}
      if (idxR == 3) {pinR=buttreadD;}
      int ChkButtonState=!digitalRead(pinR);
      int butnum=(idxW*4)+idxR;
      if (ChkButtonState != ButtonState[butnum]){
        Joystick.setButton(butnum, ChkButtonState);
        ButtonState[butnum] = ChkButtonState;
      }
    }
    digitalWrite(pinW, HIGH);
  }

  joy_ax1_val=reRange(joy_ax1_val,92,977,1);
  joy_ax2_val=reRange(joy_ax2_val,64,928,1);
  joy_ax3_val=reRange(joy_ax3_val,88,985,1);
  joy_ax4_val=reRange(joy_ax4_val,47,920,-1);
  joy_ax5_val=reRange(joy_ax5_val,15,1023,-1);
  /*Serial.println( String(joy_ax1_val)+" "+
                  String(joy_ax2_val)+" "+
                  String(joy_ax3_val)+" "+
                  String(joy_ax4_val)+" "+
                  String(joy_ax5_val)+" ");
  */
  Joystick.setXAxis(joy_ax1_val);
  Joystick.setYAxis(joy_ax2_val);
  Joystick.setRudder(joy_ax3_val);
  Joystick.setThrottle(joy_ax4_val);
  Joystick.setZAxis(joy_ax5_val);

  if (digitalRead(CHKPIN) == LOW){
    Joystick.end();
  }
  delay(5);
}


    Ignore the "HIGHPIN" and "CHKPIN" - those were there for troubleshooting. The "reRange" function may stand out as odd. The purpose of that is to calibrate the analog axis somewhat. It's not handling centering really, but it will invert an axis if needed and spread the values it does read from 0-1023. 
    e.g. A stick may read from 90-950, instead of the expected 0-1023. Taking the read value, subtracting the minimum value, then multiplying by the range I want (1023), and dividing by the range I have (950-90) gives me an effective 0-1023 analog control. 



    I'm pleased with how this turned out. It matches my headset - version 11 🎧. Two 40mm drivers in each ear, David Clark headband... The headphone builds improved with each version. Perhaps this would apply to the controller as well? 😁