Note: staff-provided content does not represent an official statement from FARGOS Development, LLC. The policy on staff-authored content can be found here.


Go back to Geoff's home page.

Thrustmaster F-22 Pro Joystick USB Conversion

Note: this is one of a set of three related notes on converting Thrustmaster F-22 Pro joysticks, Thrustmaster F-16 TQS and CH Pro Rudder Pedals from obsolete game port interfaces to modern USB-based connectivity. They use an Adafruit ItsyBitsy 32u4 5V board to interface to the existing game controller circuitry and publish the respective state as a USB Human Interface Device.

This set of conversions makes provision for a momentary push button used to trigger a self-calibration cycle. The accompanying software will react to such a button press by flashing the indicator LED rapidly and monitoring the perceived bounds of the various analog inputs. The user should exercise the limits of all of the respective axis associated with the device within the calibration period (typically 15 seconds). The obtained values are saved persistently to the device's EEPROM, thus enabling calibration to survive power cycle events.

The Thrustmaster F-22 Pro joystick was a high-quality, programmable joystick available in the 1990's that used the game port interface as well plugging into the keyboard port (either the old 5-pin DIN connector or the PS/2 port). It could be used with a Thrustmaster F-16 Throttle Quadrant System as well as rudder pedals, which made for some complicated daisy-chained wiring harnesses. Unfortunately, Microsoft stopped supported the game port interface with the release of Microsoft Vista in 2006.

The programming capabilities of the Thrustmaster F-22 Pro joystick and F-16 Throttle Quadrant System were quite feature rich. Most of the definition language was focused on emitting characters in response to button presses or movement of the various potentiometers. None of those capabilities are reproduced by this USB conversion. Instead, all of the buttons and the axes are exposed and either a modern game allows the user to customize their control setup or one can make use of third-party software such as AntiMicroX as an intermediate device layer.

Thrustmaster F-22 Pro Interfacing

Rather than make use of black-box software such as MMJoy2, the decoding of electrical signals is handled via an Arduino sketch and exposed using the Arduino Joystick library. The Arduino sketch implementing the control logic is found at the bottom of this document.

Thrustmaster F-22 Pro Cabling

The F-22 Pro Joystick contains two potentiometers that are used to to measure the amount of deflection from center on the X and Y axes. There are three wires to each potentiometer that carry the voltage in, ground and relative voltage out, which varies depending on the position of the joystick handle. Because these are soldered to original circuit board, the wires must be either cut or unsoldered from the circuit board or potentiometers. The approach documented here was to cut them off cleanly place them side-by-side into an IDC plug.

Joystick Wires
Color Function Board Pin
White +5 VDC 5V
Gray X-Axis A2
Purple Ground GND
Yellow +5 VDC 5V
Green Y-Axis A3
Blue Ground GND

The F-22 Pro joystick handle has 4 buttons and a two-position trigger along with 4 hat switches. These are represented as 22 bits of state, with a 0 indicating that the corresponding button is being depressed. The button state is transmitted using 5 wires that are connected to a chain of three 4021BE shift registers embedded in the handle. Two of the wires carry the voltage in and ground, while a third is used to latch the state when performing a measurement. The board's native Serial Peripheral Interface (SPI) hardware is used to read out 24 bits of state, which means one is forced to use the SCK and MISO pins for their intended function.

Like the joystick potentiometers, the 5 handle wires are soldered to the circuit board. The approach documented here was to cut cleanly across the 5-wire cable and position them into an IDC plug.

Button Wires
Color Function Board Pin
Brown +5 VDC 5V
Red Latch 7
Orange Clock SCK
Yellow Data MISO
Green Ground GND

Optional self-calibration is supported. A calibration cycle is triggered by depressing a normally open momentary push button switch. Feedback is provided via an indicator LED.

Calibration Wires
Function Board Pin Notes
Calibrate 10 Other side of switch is connected to ground
LED 11 Other side of LED is connected to ground

The means to connect the 11 wires is personal preference. The images shown below illustrate the use of 10-pin IDC connectors for the joystick axes and button cables and 2-pin JST connectors for the indicator LED and calibration request push button. The 5 wires from the button shift registers were cut to disconnect them from the original control board and then spliced into half of a 10-wire ribbon cable. This makes it easy to ensure proper positioning in the 10-pin IDC plug. The 6 wires comprising the joysticks were carefully positioned into another 10-pin IDC plug.

F-22 Pro Joystick Wiring Diagram
F-22 Pro Joystick Wiring Diagram
F-22 Pro Joystick innards after installation
F-22 Pro Joystick after installation
F-22 Pro Joystick Board
F-22 Pro Joystick Board Detail

F-22 Pro Joystick Code

The source code for the F-22 Pro joystick interface is illustrated below. The most current release can be retrieved from this F22joystick.ino download link. The .ino files are really C++ source with an alternate file suffix to permit association with the Arduino IDE application.

The comments within the source code provide more detail that will not be repeated here.

Modifications to boards.txt

For the purposes of programming, the Adafruit ItsyBitsy 32u4 5V is nearly identical to an Arduino Leonardo. Because we want the resulting device to appear correctly named under the Game Controllers control panel, a custom entry is added to the local boards.txt file. It is a duplicate of the leonardo board entry with changes made to the build, build.vid, build.pid and build.usb_product lines. The changes are highlighted below.

itsybitsy_f22.name=F22 Pro Thrustmaster atmega32u4
itsybitsy_f22.build.vid=0x2341
itsybitsy_f22.build.pid=0x1022
itsybitsy_f22.build.usb_product="Thrustmaster F-22 Pro"
    

If the behavior of any of the axes is the reverse of your intent, just alter the corresponding INVERT_X_AXIS or INVERT_Y_AXIS #define.

/*! \brief Thrustmaster F-22 joystick USB conversion using
 * Adafruit ItsyBitsy 32u4 5V.  https://www.adafruit.com/product/3677?
 *
 * \author Geoff Carpenter gcc@fargos.net http://www.fargos.net/gcc.html
 *
 * Requires additions to boards.txt found in:
 * "C:\Users\${USER}\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.8\boards.txt"
 * Entry is a clone of leonardo board, with modified name, build.vid,
 * build.pid and build.usb_product.

itsybitsy_f22.name=F22 Pro Thrustmaster atmega32u4
itsybitsy_f22.vid.0=0x2341
itsybitsy_f22.pid.0=0x0036
itsybitsy_f22.vid.1=0x2341
itsybitsy_f22.pid.1=0x8036
itsybitsy_f22.vid.2=0x2A03
itsybitsy_f22.pid.2=0x0036
itsybitsy_f22.vid.3=0x2A03
itsybitsy_f22.pid.3=0x8036
itsybitsy_f22.upload_port.0.vid=0x2341
itsybitsy_f22.upload_port.0.pid=0x0036
itsybitsy_f22.upload_port.1.vid=0x2341
itsybitsy_f22.upload_port.1.pid=0x8036
itsybitsy_f22.upload_port.2.vid=0x2A03
itsybitsy_f22.upload_port.2.pid=0x0036
itsybitsy_f22.upload_port.3.vid=0x2A03
itsybitsy_f22.upload_port.3.pid=0x8036
itsybitsy_f22.upload_port.4.board=leonardo

itsybitsy_f22.xupload.tool=avrdude
itsybitsy_f2x2.upload.tool.default=avrdude
itsybitsy_xf22.upload.tool.network=arduino_ota
itsybitsxy_f22.upload.protocol=avr109
itsybixtsy_f22.upload.maximum_size=28672
itsyxbitsy_f22.upload.maximum_data_size=2560
itxsybitsy_f22.upload.speed=57600
xitsybitsy_f22.upload.disable_flushing=true
itsybitsy_f22.upload.use_1200bps_touch=true
itsybitsy_f22.upload.wait_for_upload_port=true

itsybitsy_f22.bootloader.tool=avrdude
itsybitsy_f22.bootloader.tool.default=avrdude
itsybitsy_f22.bootloader.low_fuses=0xff
itsybitsy_f22.bootloader.high_fuses=0xd8
itsybitsy_f22.bootloader.extended_fuses=0xcb
itsybitsy_f22.bootloader.file=caterina/Caterina-Leonardo.hex
itsybitsy_f22.bootloader.unlock_bits=0x3F
itsybitsy_f22.bootloader.lock_bits=0x2F

itsybitsy_f22.build.mcu=atmega32u4
itsybitsy_f22.build.f_cpu=16000000L
itsybitsy_f22.build.vid=0x2341
itsybitsy_f22.build.pid=0x1022
itsybitsy_f22.build.usb_product="Thrustmaster F-22 Pro"
itsybitsy_f22.build.board=AVR_LEONARDO
itsybitsy_f22.build.core=arduino
itsybitsy_f22.build.variant=leonardo
itsybitsy_f22.build.extra_flags={build.usb_flags}

 */

/* NOTE:  button state is obtained from taking a parallel snapshot of the
 * on/off states and reading them serially from a set of 4021BE shift registers.
 * The hardware SPI interface is exploited to extract the bitstream, which means
 * one is forced to use the SCLK and MISO pins, but one does have the freedom
 * to choose the pin used to latch the state.
 *
 * The shift registers in the handle are connected via a set of five (5)
 * distinctly colored wires.
 *
 * + The brown wire is fed +5 VDC.
 * + The green wire is connected to ground.
 * + The latch/chip select signal is connected via the red wire. As noted above,
 *   there is some freedom selecting the GPIO pin to be used for this LATCH_PIN.
 * + The orange wire carries the CLOCK_PIN signal and should be connected
 *   to SCLK when using the native SPI interface.
 * + The yellow wire carries the bitstream representing the button states
 *   to the DATA_PIN and should be connected to MISO when using the
 *    native SPI hardware.
 */

/*! \brief GPIO Pin used for shift register clock */
#define CLOCK_PIN SCK  // orange wire
/*! \brief GPIO pin used to read switch state bits */
#define DATA_PIN MISO  // yellow wire
/*! \brief GPIO pin used to latch button state when held low. */
#define LATCH_PIN 7  // red

/* There are two potentiometers in the handle that are used to encode the
 * left/right and up/down axis values. Each potentiometer is connected to
 * ground at one end and the highest input signal at the other.
 * The middle tap is connected to one of the analog GPIO pins to read the
 * relative position of the joystick.  The voltage fed to the potentiometers
 * depends upon the MCU in use; 3.3 volts is common, but 5 volts would be used
 * with an Adafruit ItsyBitsy 32u4 5V.
 */

/*! \brief GPIO pin to read X-axis analog signal. */
#define X_AXIS_PIN A2
/*! \brief GPIO pin to read Y-axis analog signal. */
#define Y_AXIS_PIN A3

/*! \brief Enable manual calibration */
#define SUPPORT_CALIBRATION 1
/*! \brief GPIO pin used with momentary push button to trigger manual calibration.
The button is connected to ground and this GPIO pin. */
#define CALIBRATE_BUTTON_PIN 10
/*!\brief GPIO pin  used to drive an indicator LED. */
#define INDICATOR_LED 11

/*! \brief Amount of time manual calibration cycle lasts in milliseconds */
#define CALIBRATION_DURATION_MS (15 * 1000)
/*! \brief Sample rate per second. Not guaranteed to be achieved,
 but rate will be no faster. Used to derive POLL_DELAY_MS. */
#define SAMPLE_RATE_PER_SECOND 100
/*! brief Delay between polling cycles in milliseconds.
  Normally derived from SAMPLE_RATE_PER_SECOND.
 */
#define POLL_DELAY_MS (1000 / (SAMPLE_RATE_PER_SECOND))

/*! \brief Set to 0 if only the primary hat switch should be exposed. The remaining are
exposed as distinct buttons.*/
#define EXPOSE_ALL_HAT_SWITCHES 0
#if EXPOSE_ALL_HAT_SWITCHES == 1
#define F22_BUTTONS (6)
#define F22_HAT_SWITCHES 4 /* hat switches are reported as 0, 45, 90, 135, 180, 225. 270, 315 degrees*/
#else
#define F22_BUTTONS (6 + (3 * 4))
#define F22_HAT_SWITCHES 1 /* hat switches are reported as 0, 45, 90, 135, 180, 225. 270, 315 degrees*/
#endif

/* Unique id for device to be handed to Joystick constructor. */
#define MY_F22_JOYSTICK_ID 4

#define LOG_ENABLED 1
/* These log interfaces are compatible with the advanced thread-safe
 * logging API made available by FARGOS Development, LLC.
 * See http://www.fargos.net/documents/FARGOSutilsLibrary.html
 */
#if LOG_ENABLED > 0
#include <Streaming.h>
#if LOG_ENABLED > 2
#define LOG_COUT(level) Serial << F(__FILE__) << F(":") << __LINE__ << F("\t") << F(#level) << F("\t")
#else
#define LOG_COUT(level) Serial << F(":") << __LINE__ << F("\t") << F(#level) << F("\t")
#endif
#define LOG_ENDLINE endl
#endif

/*! \brief Console baud rate
 *
 * This value should match the baud rate selected in the Arduino IDE's
 * serial monitor window or a dedicated terminal program like Putty.
 */
#define CONSOLE_BAUD_RATE 115200

#include <Joystick.h>
#include <SPI.h>
#include <EEPROM.h>

#define MAX_ANALOG_VALUE 1023
#define ANALOG_MIDPOINT (((MAX_ANALOG_VALUE + 1) / 2) - 1)
#define MAX_AXIS_VALUE (((MAX_ANALOG_VALUE + 1) / 2) - 1)
#define MIN_AXIS_VALUE (-MAX_AXIS_VALUE)
#define INVERT_X_AXIS false
#define INVERT_Y_AXIS true

// Initialize the Joystick (with 8 buttons enabled)
static Joystick_ Joystick(MY_F22_JOYSTICK_ID,
                          JOYSTICK_TYPE_JOYSTICK, F22_BUTTONS, F22_HAT_SWITCHES,
                          true, true, false, false, false, false,
                          false, false, false, false, false);


static unsigned long startCalibrateMode_ms;

static uint8_t indicatorState;
static uint32_t indicatorBlinkCount;
static uint32_t indicatorBlinkRate;
static unsigned long indicatorBlinkUntilMillis;

static const char compiledOnDate[6 + 1] = {
  // YY year
  __DATE__[9], __DATE__[10],

  // First month letter, Oct Nov Dec = '1' otherwise '0'
  (__DATE__[0] == 'O' || __DATE__[0] == 'N' || __DATE__[0] == 'D') ? '1' : '0',

  // Second month letter
  (__DATE__[0] == 'J')   ? ((__DATE__[1] == 'a') ? '1' :  // Jan, Jun or Jul
                            ((__DATE__[2] == 'n') ? '6' : '7'))
  : (__DATE__[0] == 'F') ? '2'
                         :  // Feb
    (__DATE__[0] == 'M') ? (__DATE__[2] == 'r') ? '3' : '5'
                         :  // Mar or May
    (__DATE__[0] == 'A') ? (__DATE__[1] == 'p') ? '4' : '8'
                         :  // Apr or Aug
    (__DATE__[0] == 'S') ? '9'
                         :  // Sep
    (__DATE__[0] == 'O') ? '0'
                         :  // Oct
    (__DATE__[0] == 'N') ? '1'
                         :  // Nov
    (__DATE__[0] == 'D') ? '2'
                         :  // Dec
    0,

  // First day letter, replace space with digit
  __DATE__[4] == ' ' ? '0' : __DATE__[4],

  // Second day letter
  __DATE__[5],

  '\0'
};

#define TOTAL_AXES 2
static const uint8_t axisPin[TOTAL_AXES] = { X_AXIS_PIN, Y_AXIS_PIN };
static const bool invertAxis[TOTAL_AXES] = { INVERT_X_AXIS, INVERT_Y_AXIS };

struct AxisCalibrationData {
  uint16_t minCalibration;
  uint16_t maxCalibration;
  uint16_t centerCalibration;
};

static AxisCalibrationData calibrationData[TOTAL_AXES] = {
  { 0, MAX_ANALOG_VALUE, (MAX_ANALOG_VALUE + 1) / 2 },
  { 0, MAX_ANALOG_VALUE, (MAX_ANALOG_VALUE + 1) / 2 }
};

static void load_calibration_data(uint8_t axisId) {
  uint8_t *record = reinterpret_cast<uint8_t *>(calibrationData + axisId);
  uint8_t *base = reinterpret_cast<uint8_t *>(calibrationData);
  int offset = record - base;
  EEPROM.get(offset, calibrationData[axisId]);
  if (calibrationData[axisId].minCalibration == ~0) {  // not set in EEPROM, assign default
#if LOG_ENABLED
    LOG_COUT(info) << F("set minCalibration[") << axisId << F("] to 0") << LOG_ENDLINE;
#endif
    calibrationData[axisId].minCalibration = 0;
  }
  if (calibrationData[axisId].maxCalibration == ~0) {  // not set in EEPROM, assign default
#if LOG_ENABLED
    LOG_COUT(info) << F("set maxCalibration[") << axisId << F("] to MAX_ANALOG") << LOG_ENDLINE;
#endif
    calibrationData[axisId].maxCalibration = MAX_ANALOG_VALUE;
  }
  if (calibrationData[axisId].centerCalibration == ~0) {  // not set
#if LOG_ENABLED
    LOG_COUT(info) << F("set centerCalibration[") << axisId << F("] to midpoint") << LOG_ENDLINE;
#endif
    calibrationData[axisId].centerCalibration = (calibrationData[axisId].minCalibration + calibrationData[axisId].maxCalibration) / 2;
  }
#if LOG_ENABLED
  LOG_COUT(info) << F("loaded calibration[") << axisId << F("] min=") << calibrationData[axisId].minCalibration << F(" max=") << calibrationData[axisId].maxCalibration << F(" center=") << calibrationData[axisId].centerCalibration << LOG_ENDLINE;
#endif
}

static void save_calibration_data(uint8_t axisId) {
  uint8_t *record = reinterpret_cast<uint8_t *>(calibrationData + axisId);
  uint8_t *base = reinterpret_cast<uint8_t *>(calibrationData);
  int offset = record - base;
  EEPROM.put(offset, calibrationData[axisId]);
}

uint32_t totalReadCount;

static int32_t read_joystick_pin(uint8_t pin, uint16_t minValue, uint16_t maxValue, uint16_t midpoint, bool invert = false) {
  totalReadCount += 1;
  int16_t value = analogRead(pin);
  if (invert) {
    value = maxValue - value;
    if (value < 0) value = 0;
  }
  if (value < minValue) value = minValue;
  if (value > maxValue) value = maxValue;

  int32_t scaled_result;
  if (value < midpoint) {
    int32_t range = (midpoint - minValue) + 1;
    int32_t offset = midpoint - value;
    if (offset >= range) offset = range - 1;
    scaled_result = (offset * ANALOG_MIDPOINT) / range;
    scaled_result = ANALOG_MIDPOINT - scaled_result;
    if (scaled_result < 0) scaled_result = 0;
  } else {
    int32_t range = (maxValue - midpoint) + 1;
    int32_t offset = value - midpoint;
    if (offset >= range) offset = range - 1;
    scaled_result = (offset * ANALOG_MIDPOINT) / range;
    scaled_result += ANALOG_MIDPOINT;
    if (scaled_result > MAX_ANALOG_VALUE) scaled_result = MAX_ANALOG_VALUE;
  }
  int32_t result = (scaled_result * (MAX_ANALOG_VALUE + 1)) / ((MAX_AXIS_VALUE - MIN_AXIS_VALUE) + 1);

#if LOG_ENABLED > 3
  LOG_COUT(info) << F("pin=") << pin << F(" val=") << value << F(" minVal=") << minValue << F(" maxVal=") << maxValue << F(" midPoint=") << midpoint << F(" scaled_result=") << scaled_result << LOG_ENDLINE;
#endif
  return (result);
}



static void read_joystick_state() {
  for (uint8_t i = 0; i < TOTAL_AXES; i += 1) {
    int32_t discardValue = read_joystick_pin(axisPin[i], calibrationData[i].minCalibration,
                                             calibrationData[i].maxCalibration, calibrationData[i].centerCalibration,
                                             invertAxis[i]);
    int32_t reading = read_joystick_pin(axisPin[i], calibrationData[i].minCalibration,
                                        calibrationData[i].maxCalibration, calibrationData[i].centerCalibration,
                                        invertAxis[i]);
    int32_t scaled_value = reading + MIN_AXIS_VALUE;
#if 1
    if (scaled_value > (MAX_AXIS_VALUE - 1)) {
#if LOG_ENABLED
      LOG_COUT(info) << F("scaled result=") << scaled_value << LOG_ENDLINE;
#endif
      scaled_value = MAX_AXIS_VALUE - 1;
    }
#endif
#if LOG_ENABLED > 2
    LOG_COUT(info) << F("axis=") << i << F(" pin=") << axisPin[i] << F(" reading=") << reading << F(" scaled=") << scaled_value << LOG_ENDLINE;
#endif
    switch (i) {
      case 0:
        Joystick.setXAxis(scaled_value);
        break;
      case 1:
        Joystick.setYAxis(scaled_value);
        break;
      default:
#if LOG_ENABLED
        LOG_COUT(info) << F("no support") << LOG_ENDLINE;
#endif
        break;
    }  // end switch
  }    // end for
}

#if SUPPORT_CALIBRATION
static void monitorCalibration() {
  for (uint8_t i = 0; i < TOTAL_AXES; i += 1) {
    uint8_t pin = axisPin[i];
    uint16_t value = analogRead(pin);
    if (value > calibrationData[i].maxCalibration) calibrationData[i].maxCalibration = value;
    if (value < calibrationData[i].minCalibration) calibrationData[i].minCalibration = value;
  }
}

static bool checkForCalibration() {
  if (startCalibrateMode_ms == 0) {
    uint8_t val = digitalRead(CALIBRATE_BUTTON_PIN);
    if (val == HIGH) {
      return (false);
    }
#if LOG_ENABLED > 1
    LOG_COUT(info) << F("start calibration") << LOG_ENDLINE;
#endif
    startCalibrateMode_ms = millis();
    indicatorState = HIGH;
    digitalWrite(INDICATOR_LED, HIGH);
    indicatorBlinkRate = 10;
    // we assume joystick is centered at time of initial button press,
    // take several samples and average for calculation of center
    for (uint8_t i = 0; i < TOTAL_AXES; i += 1) {
      uint8_t pin = axisPin[i];

      int32_t aveValue = 0;
      enum { SAMPLE_COUNT = 5 };
      for (uint8_t count = 0; count < SAMPLE_COUNT; count += 1) {
        int32_t center_value = read_joystick_pin(axisPin[i], calibrationData[i].minCalibration,
                                                 calibrationData[i].maxCalibration, calibrationData[i].centerCalibration,
                                                 invertAxis[i]);
        aveValue += center_value;
      }
      calibrationData[i].centerCalibration = aveValue / SAMPLE_COUNT;
#if LOG_ENABLED
      LOG_COUT(info) << F("set center ") << i << F(" to ") << calibrationData[i].centerCalibration << LOG_ENDLINE;
#endif
      calibrationData[i].minCalibration = calibrationData[i].centerCalibration;
      calibrationData[i].maxCalibration = calibrationData[i].centerCalibration;
    }
  }
  // in calibration mode
  monitorCalibration();
  unsigned long now = millis();
  if (now > (startCalibrateMode_ms + CALIBRATION_DURATION_MS)) {
#if LOG_ENABLED > 1
    LOG_COUT(info) << F("calibration ends") << LOG_ENDLINE;
#endif
    for (uint8_t i = 0; i < TOTAL_AXES; i += 1) {
      save_calibration_data(i);
    }
    startCalibrateMode_ms = 0;  // turn off
    indicatorState = LOW;
    indicatorBlinkRate = 0;
    digitalWrite(INDICATOR_LED, LOW);
  }
}
#endif /* SUPPORT_CALIBRATION */

#define PINKY !(buttonInputs1 & 0x80)  /* Pinky Switch */
#define TG1 !(buttonInputs1 & 0x40)    /* Trigger 1 */
#define TG2 !(buttonInputs1 & 0x20)    /* Trigger 2 */
#define S1 !(buttonInputs1 & 0x10)     /* Nose Wheel Steering */
#define PADDLE !(buttonInputs1 & 0x08) /* Paddle Switch */
#define THUMB !(buttonInputs1 & 0x04)  /* Pickle */

#define H1D !(buttonInputs2 & 0x80) /* HAT */
#define H1R !(buttonInputs2 & 0x40)
#define H1U !(buttonInputs2 & 0x20)
#define H1L !(buttonInputs2 & 0x10)
#define H4U !(buttonInputs2 & 0x08) /* Castle */
#define H4L !(buttonInputs2 & 0x04)
#define H4D !(buttonInputs2 & 0x02)
#define H4R !(buttonInputs2 & 0x01)

#define H3D !(buttonInputs3 & 0x80) /* Weap */
#define H3R !(buttonInputs3 & 0x40)
#define H3U !(buttonInputs3 & 0x20)
#define H3L !(buttonInputs3 & 0x10)
#define H2D !(buttonInputs3 & 0x08) /* Target */
#define H2R !(buttonInputs3 & 0x04)
#define H2U !(buttonInputs3 & 0x02)
#define H2L !(buttonInputs3 & 0x01)

/* returns 0, 45, 90, 135, 180, 225, 270, 315 based on hat bits */
static int16_t decodeHatAngle(uint8_t up, uint8_t down, uint8_t right, uint8_t left) {
  int16_t angle = -1;

  if (up) {
    if (right) {
      angle = 45;
    } else if (left) {
      angle = 315;
    } else {
      angle = 0;
    }
  } else if (down) {
    if (right) {
      angle = 135;
    } else if (left) {
      angle = 225;
    } else {
      angle = 180;
    }
  } else if (right) {
    angle = 90;
  } else if (left) {
    angle = 270;
  }
  return (angle);
}

static void read_button_state() {
  digitalWrite(LATCH_PIN, LOW);
  uint8_t buttonInputs1 = SPI.transfer(0x00);
  uint8_t buttonInputs2 = SPI.transfer(0x00);
  uint8_t buttonInputs3 = SPI.transfer(0x00);
  digitalWrite(LATCH_PIN, HIGH);
#if LOG_ENABLED > 1
  if ((buttonInputs1 != 0xff) || (buttonInputs2 != 0xff) || (buttonInputs3 != 0xff)) {
    LOG_COUT(info) << F("in1=") << buttonInputs1 << F(" in2=") << buttonInputs2 << F(" in3=") << buttonInputs3 << LOG_ENDLINE;
  }
#endif
  Joystick.setButton(0, TG1);
  Joystick.setButton(1, THUMB);
  Joystick.setButton(2, PINKY);
  Joystick.setButton(3, PADDLE);
  Joystick.setButton(4, S1);
  Joystick.setButton(5, TG2);
  int16_t angle = decodeHatAngle(H1U, H1D, H1R, H1L);
  Joystick.setHatSwitch(0, angle);
#if EXPOSE_ALL_HAT_SWITCHES == 0
  // expose remaining hat switches as distinct buttons
  Joystick.setButton(6, H2U);
  Joystick.setButton(7, H2R);
  Joystick.setButton(8, H2D);
  Joystick.setButton(9, H2L);
  Joystick.setButton(10, H3U);
  Joystick.setButton(11, H3R);
  Joystick.setButton(12, H3D);
  Joystick.setButton(13, H3L);
  Joystick.setButton(14, H4U);
  Joystick.setButton(15, H4R);
  Joystick.setButton(16, H4D);
  Joystick.setButton(17, H4L);
#else
  // expose remaining 3 hat switches
  angle = decodeHatAngle(H2U, H2D, H2R, H2L);
  Joystick.setHatSwitch(1, angle);
  angle = decodeHatAngle(H3U, H3D, H3R, H3L);
  Joystick.setHatSwitch(2, angle);
  angle = decodeHatAngle(H4U, H4D, H4R, H4L);
  Joystick.setHatSwitch(3, angle);
#endif
}

void setup() {
#if LOG_ENABLED
  // Setup hardware serial port
  Serial.begin(CONSOLE_BAUD_RATE);
  delay(500);  // stabilize after power-on
  unsigned long start = millis();
  while (!Serial) {
    unsigned long delayed = millis() - start;
    if (delayed > 5000) break;
  }
  LOG_COUT(info) << F("Thrustmaster F-22 Pro Joystick firmware compiled on date ") << compiledOnDate << LOG_ENDLINE;
  LOG_COUT(info) << F("Original from Geoff Carpenter gcc@fargos.net http://www.fargos.net/gcc.html") << LOG_ENDLINE;
#endif
  pinMode(LATCH_PIN, OUTPUT);
  digitalWrite(LATCH_PIN, HIGH);  // default state is high
  pinMode(CLOCK_PIN, OUTPUT);
  pinMode(DATA_PIN, INPUT);

  pinMode(X_AXIS_PIN, INPUT);
  pinMode(Y_AXIS_PIN, INPUT);

  pinMode(LED_BUILTIN, OUTPUT);
#if SUPPORT_CALIBRATION
  pinMode(CALIBRATE_BUTTON_PIN, INPUT_PULLUP);
  pinMode(INDICATOR_LED, OUTPUT);
  digitalWrite(INDICATOR_LED, LOW);
  indicatorBlinkRate = 100;
  indicatorBlinkUntilMillis = millis() + 10000;
  for (uint8_t i = 0; i < TOTAL_AXES; i += 1) {
    load_calibration_data(i);
  }
#endif /* SUPPORT CALIBRATION */

  Joystick.begin(false);  // Set auto-send to false for better performance
  Joystick.setXAxisRange(MIN_AXIS_VALUE, MAX_AXIS_VALUE);
  Joystick.setYAxisRange(MIN_AXIS_VALUE, MAX_AXIS_VALUE);
#if LOG_ENABLED
  LOG_COUT(info) << F("clock=") << CLOCK_PIN << F(" ss=") << LATCH_PIN << F(" miso=") << DATA_PIN << LOG_ENDLINE;
#endif
  SPI.begin();
}

static bool blinkLED(unsigned long currentTime) {
  static uint32_t count;
  static uint8_t ledState;

  count += 1;
  if (count < 100) return (false);
  // toggle LED state
  count = 0;
  ledState = 1 - ledState;
  digitalWrite(LED_BUILTIN, ledState);
  //    digitalWrite(LATCH_PIN, ledState);

  return (true);
}

#if SUPPORT_CALIBRATION
static bool blinkIndicatorLED(unsigned long currentTime) {
  bool changed = false;

  if (indicatorBlinkUntilMillis != 0) {
    if (indicatorBlinkUntilMillis <= currentTime) {  // reached end of cycle
      changed = indicatorState;
      indicatorBlinkUntilMillis = 0;  // turn off
      indicatorBlinkRate = 0;
      indicatorState = LOW;
      digitalWrite(INDICATOR_LED, LOW);
    }
  }
  if (indicatorBlinkRate != 0) {
    indicatorBlinkCount += 1;
    if (indicatorBlinkCount >= indicatorBlinkRate) {
      indicatorBlinkCount = 0;
      indicatorState = 1 - indicatorState;
      digitalWrite(INDICATOR_LED, indicatorState);
      changed = true;
    }
  }
  return (changed);
}
#endif /* SUPPORT_CALIBRATION */

static unsigned long delayIfNeeded(unsigned long now) {
  static unsigned long lastTime;

  unsigned long nextTime = lastTime + POLL_DELAY_MS;
  if (now < nextTime) {
    delay(nextTime - now);
    now = millis();
  }
  lastTime = now;
  return (now);
}

void loop() {
  unsigned long now = delayIfNeeded(millis());
  blinkLED(now);
#if SUPPORT_CALIBRATION
  blinkIndicatorLED(now);
  checkForCalibration();
#endif
  read_button_state();
  read_joystick_state();

  // Send the updated states to the PC all at once
  Joystick.sendState();
}
/* vim: set expandtab shiftwidth=4 tabstop=4: */