1. Start Simple

This is a fully functional joystick with 2 axes, 2 buttons and a single hat switch.

"""
JoystickXL Example #1 - Start Simple (2 axes, 2 buttons, 1 hat switch).

Tested on an Adafruit ItsyBitsy M4 Express, but should work on other CircuitPython
boards with a sufficient quantity/type of pins.

* Buttons are on pins D9 and D10
* Axes are on pins A2 and A3
* Hat switch is on pins D2 (up), D3 (down), D4 (left) and D7 (right)

Don't forget to copy boot.py from the example folder to your CIRCUITPY drive.
"""

import board  # type: ignore (this is a CircuitPython built-in)
from joystick_xl.inputs import Axis, Button, Hat
from joystick_xl.joystick import Joystick

joystick = Joystick()

joystick.add_input(
    Button(board.D9),
    Button(board.D10),
    Axis(board.A2),
    Axis(board.A3),
    Hat(up=board.D2, down=board.D3, left=board.D4, right=board.D7),
)

while True:
    joystick.update()

2. More Inputs!

This is a fully functional joystick with 8 axes, 24 buttons and 4 hat switches. Notice the only difference between this example and the Start Simple example is the number of inputs added with add_input.

"""
JoystickXL Example #2 - More Inputs! (8 axes, 24 buttons, 4 hat switches).

Tested on an Adafruit Grand Central M4 Express, but should work on other CircuitPython
boards with a sufficient quantity/type of pins.

* Buttons are on pins D22-D45
* Axes are on pins A8-A15
* Hat switches are on pins D2-D9 and D14-D21

Don't forget to copy boot.py from the example folder to your CIRCUITPY drive.
"""

import board  # type: ignore (this is a CircuitPython built-in)
from joystick_xl.inputs import Axis, Button, Hat
from joystick_xl.joystick import Joystick

joystick = Joystick()

joystick.add_input(
    Button(board.D22),
    Button(board.D23),
    Button(board.D24),
    Button(board.D25),
    Button(board.D26),
    Button(board.D27),
    Button(board.D28),
    Button(board.D29),
    Button(board.D30),
    Button(board.D31),
    Button(board.D32),
    Button(board.D33),
    Button(board.D34),
    Button(board.D35),
    Button(board.D36),
    Button(board.D37),
    Button(board.D38),
    Button(board.D39),
    Button(board.D40),
    Button(board.D41),
    Button(board.D42),
    Button(board.D43),
    Button(board.D44),
    Button(board.D45),
    Axis(board.A8),
    Axis(board.A9),
    Axis(board.A10),
    Axis(board.A11),
    Axis(board.A12),
    Axis(board.A13),
    Axis(board.A14),
    Axis(board.A15),
    Hat(up=board.D2, down=board.D3, left=board.D4, right=board.D5),
    Hat(up=board.D6, down=board.D7, left=board.D8, right=board.D9),
    Hat(up=board.D14, down=board.D15, left=board.D16, right=board.D17),
    Hat(up=board.D18, down=board.D19, left=board.D20, right=board.D21),
)

while True:
    joystick.update()

3. Button Operations

This example shows some of the options available for detecting, processing and bypassing button presses, which can be useful when you want to start adding things like LEDs and other sensors to your custom controller.

"""
JoystickXL Example #3 - Button Operations (2 axes, 2 buttons).

This example demonstrates the use of button `bypass`, `is_pressed`, `is_released`,
`was_pressed` and `was_released` attributes.

Tested on an Adafruit ItsyBitsy M4 Express, but should work on other CircuitPython
boards with a sufficient quantity/type of pins.

* Buttons are on pins D9 and D10
* Axes are on pins A2 and A3
* A "safety switch" is connected to pin D11
* An LED and current-limiting resistor are connected to pin D12
* The on-board LED connected to pin D13 is used as well

Don't forget to copy boot.py from the example folder to your CIRCUITPY drive.
"""

import board  # type: ignore (this is a CircuitPython built-in)
import digitalio  # type: ignore (this is a CircuitPython built-in)
from joystick_xl.inputs import Axis, Button
from joystick_xl.joystick import Joystick

joystick = Joystick()

joystick.add_input(
    Button(board.D9),  # primary fire
    Button(board.D10),  # secondary fire
    Axis(board.A2),  # x-axis
    Axis(board.A3),  # y-axis
)

# The safety switch will be used to lock out the first two buttons, which - on a typical
# flight stick - are the fire buttons for primary and secondary weapons systems.
safety_switch = digitalio.DigitalInOut(board.D11)
safety_switch.direction = digitalio.Direction.INPUT
safety_switch.pull = digitalio.Pull.UP

# This will be used to demonstrate the `is_pressed` attribute - it will stay lit while
# button 1 is pressed.
b1_led = digitalio.DigitalInOut(board.D12)
b1_led.direction = digitalio.Direction.OUTPUT

# This will be used to demonstrate the `is_released` attribute - it will stay lit while
# button 2 is released.
b2_led = digitalio.DigitalInOut(board.D13)
b2_led.direction = digitalio.Direction.OUTPUT


while True:

    # Set the bypass value of the first two buttons based on the safety switch value.
    for b in range(2):
        joystick.button[b].bypass = safety_switch.value

    # Axes and hat switches can also be bypassed:
    #   joystick.axis[0].bypass = True
    #   joystick.hat[2] = True

    # Hat switch buttons can also be individually bypassed like so:
    #   joystick.hat[1].up.bypass = True
    #   joystick.hat[1].right.bypass = True

    # Update the leds using `is_pressed` and `is_released`.  Don't forget the list of
    # buttons is zero-based, so button 1 is joystick.button[0]!
    b1_led.value = joystick.button[0].is_pressed
    b2_led.value = joystick.button[1].is_released

    # Notice that the `*_pressed` and `*_released` events are not affected by the
    # `bypass` attribute - `bypass` only affects the state of the button that is sent
    # to the host device via USB.  If you need to stop local button-related functions
    # (such as the LED controls above), you can wrap it in an if statement like the
    # following (You'll need to be connected via serial console to see the output of
    # the print statement):

    if joystick.button[1].bypass is False:
        if joystick.button[1].is_pressed:
            print("Button 2 is pressed and is not bypassed.")

    # If you want events to occur only on the rising/falling edge of button presses,
    # you can use the `was_pressed` and `was_released` attributes. (You'll need to be
    # connected via serial console to see the output of these print statements.)
    if joystick.button[0].was_pressed:
        print("Button 1 was just pressed.")

    if joystick.button[0].was_released:
        print("Button 1 was just released.")

    # Update all of the joystick inputs.
    joystick.update()

4. GPIO Expander

If you find yourself running out of GPIO pins on your CircuitPython board, you can add I/O expander peripherals to get the extra pins you need. The Microchip MCP23017 is ideal, as Adafruit has a CircuitPython driver for it that lets us treat the inputs almost exactly like on-board pins.

Check out Adafruit’s MCP23017 CircuitPython Guide for more information on how to use this peripheral device.

"""
JoystickXL Example #4 - GPIO Expander (8 buttons and 1 hat switch).

This example uses a Microchip MCP23017-E/SP I/O expander
(https://www.adafruit.com/product/732), and requires the `adafruit_mcp230xx` and
`adafruit_bus_device` libraries from the CircuitPython Library Bundle.

Tested on an Adafruit ItsyBitsy M4 Express, but should work on other CircuitPython
boards with a sufficient quantity/type of pins.

* 3V from CircuitPython board to MCP23017 Vdd and !Reset pins
* G from CircuitPython board to MCP23017 Vss and address (A0, A1, A2) pins
* SCL, SDA from CircuitPython board to MCP23017 (with 10k pull-up resistors)
* Buttons are on MCP23017 pins GPA0-GPA7
* Hat Switch is on MCP23017 pins GPB0-GPB3 (GPB0=UP, GPB1=DOWN, GPB2=LEFT, GPB3=RIGHT)

Don't forget to copy boot.py from the example folder to your CIRCUITPY drive.
"""


import board  # type: ignore (this is a CircuitPython built-in)
import busio  # type: ignore (this is a CircuitPython built-in)
import digitalio  # type: ignore (this is a CircuitPython built-in)
from adafruit_mcp230xx.mcp23017 import MCP23017
from joystick_xl.inputs import Button, Hat
from joystick_xl.joystick import Joystick

# Set up I2C MCP23017 I/O expander
i2c = busio.I2C(board.SCL, board.SDA)
mcp = MCP23017(i2c)

# JoystickXL doesn't know how to configure I/O pins on peripheral devices like it does
# with on-board pins, so we'll have to do the set up manually here.
for i in range(12):
    pin = mcp.get_pin(i)
    pin.direction = digitalio.Direction.INPUT
    pin.pull = digitalio.Pull.UP

# Set up JoystickXL!
js = Joystick()

for i in range(8):
    js.add_input(Button(mcp.get_pin(i)))

js.add_input(
    Hat(
        up=mcp.get_pin(8),
        down=mcp.get_pin(9),
        left=mcp.get_pin(10),
        right=mcp.get_pin(11),
    )
)

while True:
    js.update()

5. External ADC

Similar to the previous example, this one shows how to use an external analog-to-digital convertor (Microchip MCP3008) to get additional inputs for axes.

Check out Adafruit’s MCP3008 CircuitPython Guide for more information on how to use this peripheral device.

"""
JoystickXL Example #5 - External Analog-to-Digital Converter (8 axes).

This example uses a Microchip MCP3008-I/P analog-to-digital converter
(https://www.adafruit.com/product/856), and requires the `adafruit_mcp3xxx` and
`adafruit_bus_device` libraries from the CircuitPython Library Bundle.

Tested on an Adafruit ItsyBitsy M4 Express, but should work on other CircuitPython
boards with a sufficient quantity/type of pins.

* 3V from CircuitPython board to MCP3008 Vdd and Vref
* G from CircuitPython board to MCP3008 AGND and DGND
* MOSI from CircuitPython board to MCP3008 Din
* MISO from CircuitPython board to MCP3008 Dout
* SCK from CircuitPython board to MCP3008 CLK
* D7 from CircuitPython board to MCP3008 !CS/SHDN
* Axes are on MCP3008 pins CH0-CH7

Don't forget to copy boot.py from the example folder to your CIRCUITPY drive.
"""

import adafruit_mcp3xxx.mcp3008 as MCP
import board  # type: ignore (this is a CircuitPython built-in)
import busio  # type: ignore (this is a CircuitPython built-in)
import digitalio  # type: ignore (this is a CircuitPython built-in)
from adafruit_mcp3xxx.analog_in import AnalogIn
from joystick_xl.inputs import Axis
from joystick_xl.joystick import Joystick

# Set up SPI MCP3008 Analog-to-Digital converter
spi = busio.SPI(clock=board.SCK, MISO=board.MISO, MOSI=board.MOSI)
cs = digitalio.DigitalInOut(board.D7)
mcp = MCP.MCP3008(spi, cs)

# Set up JoystickXL!
js = Joystick()

js.add_input(
    Axis(AnalogIn(mcp, MCP.P0)),
    Axis(AnalogIn(mcp, MCP.P1)),
    Axis(AnalogIn(mcp, MCP.P2)),
    Axis(AnalogIn(mcp, MCP.P3)),
    Axis(AnalogIn(mcp, MCP.P4)),
    Axis(AnalogIn(mcp, MCP.P5)),
    Axis(AnalogIn(mcp, MCP.P6)),
    Axis(AnalogIn(mcp, MCP.P7)),
)

while True:
    js.update()

6. Capacitive Touch

Adding capacitive touch inputs is simple when you use a device with an existing CircuitPython driver, such as the Adafruit MPR121 Capacitive Touch Breakout.

Check out Adafruit’s MPR121 Breakout Guide for more information on how to use this peripheral device.

"""
JoystickXL Example #6 - Capacitive Touch (8 buttons and 1 hat switch).

This example uses an MPR121 12-Key Capacitive Touch Sensor Breakout
(https://www.adafruit.com/product/1982), and requires the `adafruit_mpr121` and
`adafruit_bus_device` libraries from the CircuitPython Library Bundle.

Tested on an Adafruit Metro M4 Express, but should work on other CircuitPython
boards with a sufficient quantity/type of pins.

* 3V, G, SCL, SDA from CircuitPython board to MPR121 board
* Buttons are on MPR121 inputs 0-7
* Hat Switch is on MPR121 inputs 8-11 (8=UP, 9=DOWN, 10=LEFT, 11=RIGHT)

Don't forget to copy boot.py from the example folder to your CIRCUITPY drive.
"""

import adafruit_mpr121
import board  # type: ignore (this is a CircuitPython built-in)
import busio  # type: ignore (this is a CircuitPython built-in)
from joystick_xl.inputs import Button, Hat
from joystick_xl.joystick import Joystick

# Set up I2C MPR121 capacitive touch sensor
i2c = busio.I2C(board.SCL, board.SDA)
mpr121 = adafruit_mpr121.MPR121(i2c)

# Set up JoystickXL!
js = Joystick()

# The MPR121 library returns True when a capacitive touch channel is activated.  This
# makes it "active high", so we set `active_low` to False
for i in range(8):
    js.add_input(Button(mpr121[i], active_low=False))

js.add_input(
    Hat(
        up=mpr121[8],
        down=mpr121[9],
        left=mpr121[10],
        right=mpr121[11],
        active_low=False,
    )
)

while True:
    js.update()

7. Multi-Unit HOTAS

This is a much more complicated example that uses a pair of Adafruit Grand Central M4 Express boards to create a HOTAS. (If you have no idea what that is, check out the Thrustmaster Warthog or Logitech X56.)

The HOTAS example consists of two physically separate units - the Throttle and the Stick. Each component could have its own USB connection to the host computer such that the pair appear as two independant USB HID devices, but this can get complicated because:

  1. CircuitPython USB HID game controllers (joystick/gamepad) devices all identify themselves to the operating system as CircuitPython HID, which makes it difficult to determine which device is which when more than one device is connected.

  2. A number of games/flight sims/racing sims make it difficult to distinguish between multiple controllers, which makes it challenging to get those controls configured properly and consistently.

To alleviate these issues, this example uses a wired (UART) connection between the Throttle and Stick, and a single USB connection from the Stick to the host computer. Each piece has 16 buttons, 4 axes and 2 hat switches, but the whole collection appears to the host computer as a single 32 button, 8 axis, 4 hat switch joystick.

This example makes use of JoystickXL’s virtual inputs, which allow raw input values to be assigned to them in code rather then read directly from GPIO pins.

If you look closely, you’ll notice that the only really complicated parts of this example are the bits that deal with the serial communications and the associated data processing. Everything else is almost identical to the Start Simple example above - create a JoystickXL object, associate inputs with it and make sure you call the joystick’s update() method in your main loop.

"""
JoystickXL Example #6 - HOTAS (Hands On Throttle And Stick) Stick Component.

Tested on an Adafruit Grand Central M4 Express, but should work on other CircuitPython
boards with a sufficient quantity/type of pins.

* Stick buttons are on pins D22-D37
* Stick axes are on pins A8-A11
* Stick hat switches are on pins D14-D21

The stick board needs to be connected to the throttle board as follows:

* Stick TX to Throttle RX
* Stick RX to Throttle TX
* Stick GND to Throttle GND

Don't forget to copy boot.py from the example folder to your CIRCUITPY drive.
"""

import struct

import board  # type: ignore (This is a CircuitPython built-in)
import busio  # type: ignore (This too!)
from joystick_xl.inputs import Axis, Button, Hat
from joystick_xl.joystick import Joystick

# Prepare serial (UART) comms to communicate with throttle component.
uart = busio.UART(board.TX, board.RX, baudrate=115200, timeout=0.1)

# Serial protocol constants.
STX = 0x02  # start-of-transmission
ETX = 0x03  # end-of-transmission
REQ = 0x05  # data request

# Axis configuration constants
AXIS_DB = 2500  # Deadband to apply to axis center points.
AXIS_MIN = 250  # Minimum raw axis value.
AXIS_MAX = 65285  # Maximum raw axis value.

# Prepare USB HID HOTAS device.
hotas = Joystick()

hotas.add_input(
    # The first 16 buttons are local I/O associated with the stick component, which
    # connects to the host via USB.
    Button(board.D22),
    Button(board.D23),
    Button(board.D24),
    Button(board.D25),
    Button(board.D26),
    Button(board.D27),
    Button(board.D28),
    Button(board.D29),
    Button(board.D30),
    Button(board.D31),
    Button(board.D32),
    Button(board.D33),
    Button(board.D34),
    Button(board.D35),
    Button(board.D36),
    Button(board.D37),
    # The last set of 16 buttons are virtual I/O associated with the throttle
    # component, which connects to the stick component via serial.
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    Button(),
    # The first 4 axes are local I/O associated with the stick component.
    Axis(board.A8, deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX),
    Axis(board.A9, deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX),
    Axis(board.A10, deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX),
    Axis(board.A11, deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX),
    # The last 4 axes are virtual I/O associated with the throttle component.
    Axis(deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX),
    Axis(deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX),
    Axis(deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX),
    Axis(deadband=AXIS_DB, min=AXIS_MIN, max=AXIS_MAX),
    # The first 2 hat switches are local I/O associated with the stick component.
    Hat(up=board.D14, down=board.D15, left=board.D16, right=board.D17),
    Hat(up=board.D18, down=board.D19, left=board.D20, right=board.D21),
    # The last 2 hat switches are virtual I/O associated with the throttle component.
    Hat(),
    Hat(),
)

# Stick and Throttle input processing loop.
while True:

    # Request raw state data from the throttle component.
    uart.write(bytearray((STX, REQ, ETX)))

    # we're looking for exactly 15 bytes of data from the throttle:
    #   Byte  0    = STX
    #   Byte  1    = REQ
    #   Bytes 2-3  = 16 bits of button data
    #   Bytes 4-11 = 4 x 16 bits of axis data
    #   Byte  12   = 2 x 4 bits of hat switch data
    #   Byte  13   = Checkbyte (for rudimentary error checking)
    #   Byte  14   = ETX
    rx = uart.read(15)

    if rx is not None and len(rx) == 15:

        # Calculate correct checkbyte value by XORing all command and data bytes.
        # Framing bytes (STX/ETX) and the incoming checkbyte are excluded.
        checkbyte = 0x00
        for b in rx[1:13]:
            checkbyte ^= b

        # Continue processing if framing and checkbyte are correct.
        if rx[0] == STX and rx[14] == ETX and rx[13] == checkbyte:

            # At this point there is only one 'command' to process - a data request.
            if rx[1] == REQ:

                # Unpack raw data.
                data = struct.unpack_from("<HHHHHB", rx, offset=2)

                # Update button virtual inputs with raw states from throttle.
                for i in range(16):
                    hotas.button[i + 16].source_value = (data[0] >> i) & 0x01

                # Update axis virtual inputs with raw states from throttle.
                for i in range(4):
                    hotas.axis[i + 4].source_value = data[1 + i]

                # update hat switch virtual inputs with raw states from throttle.
                for i in range(2):
                    hotas.hat[i + 2].unpack_source_values(data[5] >> (4 * i))

    # At this point we have collected all remote data and can update everything.
    hotas.update()
"""
JoystickXL Example #6 - HOTAS (Hands On Throttle And Stick) Throttle Component.

Tested on an Adafruit Grand Central M4 Express, but should work on other CircuitPython
boards with a sufficient quantity/type of pins.

* Throttle buttons are on pins D22-D37
* Throttle axes are on pins A8-A11
* Throttle hat switches are on pins D14-D21

The throttle board needs to be connected to the stick board as follows:

* Throttle TX to Stick RX
* Throttle RX to Stick TX
* Throttle GND to Stick GND

You don't need to copy boot.py to the throttle component - all USB HID communication
is handled by the stick component.
"""

import struct

import board  # type: ignore (This is a CircuitPython built-in)
import busio  # type: ignore (This too!)
from joystick_xl.inputs import Axis, Button, Hat

# Prepare serial (UART) comms to communicate with stick component.
uart = busio.UART(board.TX, board.RX, baudrate=115200)

# Serial protocol constants.
STX = 0x02  # start-of-transmission
ETX = 0x03  # end-of-transmission
REQ = 0x05  # data request

# Set up local I/O.
buttons = [
    Button(board.D22),
    Button(board.D23),
    Button(board.D24),
    Button(board.D25),
    Button(board.D26),
    Button(board.D27),
    Button(board.D28),
    Button(board.D29),
    Button(board.D30),
    Button(board.D31),
    Button(board.D32),
    Button(board.D33),
    Button(board.D34),
    Button(board.D35),
    Button(board.D36),
    Button(board.D37),
]

axes = [
    # We are only interested in raw values here - deadband/min/max processing will
    # be handled in the code running on the stick component.
    Axis(board.A8),
    Axis(board.A9),
    Axis(board.A10),
    Axis(board.A11),
]

hats = [
    Hat(up=board.D14, down=board.D15, left=board.D16, right=board.D17),
    Hat(up=board.D18, down=board.D19, left=board.D20, right=board.D21),
]

# Throttle input processing loop.
while True:

    # There's nothing to do unless we've received at least 3 bytes.
    #   Byte 0 = STX
    #   Byte 1 = Command Byte (Current only REQ is implemented)
    #   Byte 2 = ETX
    if uart.in_waiting >= 3:
        rx = uart.read(3)  # Read 3 bytes (stx, command, etx).

        # Make sure no framing error has occurred (missing STX or ETX).
        if rx[0] != STX or rx[2] != ETX:
            uart.reset_input_buffer()
            continue

        # Process state request command.
        if rx[1] == REQ:

            # Collect raw button states.
            button_states = 0
            for i, b in enumerate(buttons):
                if b.source_value:
                    button_states |= 0x01 << i

            # Collect raw hat switch states.
            hat_states = 0
            for i, h in enumerate(hats):
                hat_states |= h.packed_source_values << (4 * i)

            # Pack up all the data in a byte array.
            tx = bytearray(15)
            struct.pack_into(
                "<BBHHHHHBBB",
                tx,
                0,
                STX,
                REQ,
                button_states,
                axes[0].source_value,
                axes[1].source_value,
                axes[2].source_value,
                axes[3].source_value,
                hat_states,
                0x00,  # checkbyte placeholder
                ETX,
            )

            # Calculate the byte used by the stick component to ensure data integrity.
            checkbyte = 0x00
            for b in tx[1:13]:
                checkbyte ^= b
            tx[13] = checkbyte

            # Send the data to the stick component
            uart.write(tx)