4. Custom Modules

While common configuration tasks can be performed directly via the UI, more advanced and specialised configurations may either only be possible or much simpler by using a custom module. Each module is a simple Python script which defines the functions (callbacks) triggered in reaction to inputs being used. Since the functions are written in Python there is no limit as to what can be expressed. The following section assumes some basic familiarity with Python. We start with a general overview of the layout of a custom module in Section 4.1 which is followed by an quick overview of the API in Section 4.2 followed by the description of the decorator based callback system in Section 4.3 with periodic function callbacks described in Section 4.4. Some words on how to debug custom modules is provided in Section 4.5. Finally, Section 4.6 provides a few practical examples.

4.1 Principle & Layout of Custom Modules

Joystick Gremlin uses callbacks, i.e. functions that are executed in reaction to user inputs such as key presses or axis motion. These callbacks have access to some convenience functions which allow accessing and controlling commonly used parts of the system, such as setting the value of vJoy devices or retrieving keyboard and joystick states. Combining these readily available functions with custom code allows the implementation of varied functionality.

The general structure of a callback is as follows:

@decorator_function(<input name>)
def callback_function(event, <optional parameter list>):
    <callback implementation>

The event parameter contains information about the event that triggered the execution of the function. Each event is of type gremlin.event_handler.Event and contains the following data:

Event
event_type
The type of the event this represents.
identifier
The identifier of the event source.
hardware_id
Hardware ID assigned to the deivce that created the event.
windows_id
Index assigned by Windows to the device that created the event.
is_pressed
If the event represents a button or key the value is True for pressed and False for released state.
value
Value of an axis or hat. In case of an axis the value is in the range \(\left[-1, 1\right]\) and in the case of a hat a tuple (x direction, y direction) is used. This field's value is only valid for joystick axes and hats.
raw_value
The raw axis value, this field is only valid for joystick axes.

From this list the only values that are typically of interest are the is_pressed and value entries depending on the input type.

4.2 Device Access API

The following describes the API of the optional variables exposed via the decorator plugin framework. The plugins provide access to commonly used information by simply adding a properly named parameter to the callback function.

These parameters must be listed after the event parameter in the case of user input callbacks.

vJoy

Any decorated function that has a parameter named vjoy in its parameter list will have access to all vJoy devices. Accessing a specific VJoy instance is done by indexing the vjoy object. This object then allows setting the state of inputs by indexing the member variables axis, button, and hat. All indices start with 1. The following demonstrates typical usage:

# Access the first vJoy device and press the third button
vjoy[1].button(3).is_pressed = True

# Access the second vJoy device and move the Y axis to -0.25
vjoy[2].axis(AxisName.Y).value = -0.25
# or equivalently
vjoy[2].axis(2).value = -0.25

# Access the first vJoy device and move the first hat to
# the top right position
vjoy[1].hat(1).direction = (1, 1)

Joystick State

Any decorated function that has a parameter named joy in its parameter list will have access to all joystick devices via that variable.

Accessing a specific joystick
In order to access a specific joystick its system id needs to be known. Using the device's system id as index the joystick can be accessed by:

joystick_device = joy[system_id]
If no duplicate devices are present another option to access a specific joystick is to use its name as follows:
joystick_device = joy["T.16000M"]

Reading axis value
To read the current value of a joystick axis both the index of the axis as well as the system id of the joystick, starting with 1, are needed, with these the axis value is obtained as:

axis_value = joy[system_id].axis(axis_index).value

Reading button state
To read the current state of a button both the joystick's system id as well as index of the button, starting at 1, are needed. The following then reads the button state:

state = joy[system_id].button(button_id).is_pressed

Reading hat position
To read the current position of a hat both the joystick's system id and hat index, starting at 1, are needed. The position of the hat is reported as a \((x, y)\) tuple \(x, y \in \left\{-1, 0, 1\right\}\). A \(x\) value of 1 is right and -1 left while a value of 1 for \(y\) means up and -1 down. A value of \(0\) represents a centred position. The value is read as follows:

position = joy[system_id].hat(hat_id).direction

Keyboard State

Any decorated function that has a parameter named keyboard in its parameter list will have access to the state of all keyboard keys.

Reading key state
To read the key state the string representation of the key or the gremlin.macro.Key instance corresponding to the key is needed. Both can be found in the gremlin.macro module. Reading the state is then done as follows:

is_pressed = keyboard.is_pressed(key)

4.3 User Input Callback Generation

Callbacks reacting to user inputs are created by decorating functions using specific decorators. Here are two useful links if you're not familiar with decorators, official PEP and an exhaustive StackOverflow answer There are two types of decorators, one for joysticks and one for the keyboard. Joystick decorators are created for specific devices using the gremlin.input_devices.JoystickDecorator class as follows:

joystick_decorator = gremlin.input_devices.JoystickDecorator(
    <device name>,
    <device id>,
    <mode>
)

The value of <device id> depends on whether or not multiple devices of the same type are being used. In the case of multiple identical devices device_id consists of the tuple of the hardware_id and windows_id, otherwise, only the hardware_id is used. An object created in this way has three decorators customised for the given joystick and mode, which can be used as follows:

@joystick_decorator.axis(1)
def axis_callback(event):
    pass

@joystick_decorator.button(4)
def button_callback(event):
    pass

@joytick_decorator.hat(2)
def hat_callback(event):
    pass

The keyboard decorator can be used directly as follows:

@gremlin.input_devices.keyboard(<key name>, <mode>)
def keyboard_callback(event):
    pass

Where <key name> can be either a string representation of the key's name as or an instance of gremlin.marco.Key which are both defined in the gremlin.macro module.

The event parameter of the decorated function is always required and contains the state of the input that triggered the callback, the contents of the variable are described in Section 4.1.

4.4 Periodic Function Callbacks

In some situations a function needs to be executed at regular intervals. This is facilitated by a decorator that ensures that the function is run at a specified interval while Joystick Gremlin is active.

The decorator takes a single argument that indicates the interval, i.e. the duration, between executions of the function in seconds. The callback function can use the same plugin system as the user input callbacks to gain access to device information, e.g. vjoy, joy, and keyboard. A generic example of periodic function callback is shown below.

@gremlin.input_devices.periodic(<seconds>)
def periodic_function():
    pass

4.5 Debugging

To facilitate the debugging of custom modules without setting up the source code of Joystick Gremlin in an IDE the logging function gremlin.util.log() can be used. This stores the provided text to the user log file which can be viewed directly in Joystick Gremlin via the Tools -> Log display option.

For more detailed debugging Joystick Gremlin needs to be run from within an IDE by getting the development environment setup. While this provides the best debugging experience it also involves the most work, thus for simple tasks the logging approach may be preferable.

4.6 Examples

In the following a few examples of custom modules are shown. They provide an illustration of some of the things that can be achieved thanks to the combination of Joystick Gremlin provided functions and custom Python code.

Keyboard Controlled Throttle

This script allows the user to control an analogue throttle in 1/3rd increments using the 1, 2, 3, and 4 number keys.

import gremlin
from vjoy.vjoy import AxisName

def set_throttle(vjoy, value):
    vjoy[1].axis(AxisName.Z).value = value

@gremlin.input_devices.keyboard("1", "Global")
def throttle_0(event, vjoy):
    if event.is_pressed:
        set_throttle(vjoy, -1.0)

@gremlin.input_devices.keyboard("2", "Global")
def throttle_33(event, vjoy):
    if event.is_pressed:
        set_throttle(vjoy, -0.33)

@gremlin.input_devices.keyboard("3", "Global")
def throttle_66(event, vjoy):
    if event.is_pressed:
        set_throttle(vjoy, 0.33)

@gremlin.input_devices.keyboard("4", "Global")
def throttle_100(event, vjoy):
    if event.is_pressed:
        set_throttle(vjoy, 1.0)

Joystick Response Curve

This script configures a response curve which provides more control around the centre position and uses it for the X and Y axis of the joystick.

import gremlin
from gremlin.spline import CubicSpline
from vjoy.vjoy import AxisName

chfs = gremlin.input_devices.JoystickDecorator(
    "CH Fighterstick USB",
    2382820288,
    "Global"
)

curve = CubicSpline([
    (-1.0, -1.0),
    (-0.5, -0.25),
    ( 0.0,  0.0),
    ( 0.5,  0.25),
    ( 1.0,  1.0)
])

@chfs.axis(1)
def pitch(event, vjoy):
    vjoy[1].axis(AxisName.X).value = curve(event.value)

@chfs.axis(2)
def yaw(event, vjoy):
    vjoy[1].axis(AxisName.Y).value = curve(event.value)

Mode Switching

This script presents a few different ways of using mode switching functionalities. The first callback switches to the Radio mode while the button is being held down and switches back to the previous mode once the button is released. The next callback cycles through the Global, Radio, and Landing modes with each button press. The last callback switches directly to the Global mode when the button is pressed.

import gremlin

chfs = gremlin.input_devices.JoystickDecorator(
    "CH Fighterstick USB",
    2382820288,
    "Global"
)

mode_list = gremlin.control_action.ModeList(
        ["Global", "Radio", "Landing"]
)

@chfs.button(10)
def temporary_mode_switch(event):
    if event.is_pressed:
        gremlin.control_action.switch_mode("Radio")
    else:
        gremlin.control_action.switch_to_previous_mode()

@chfs.button(11)
def cycle_modes(event):
    if event.is_pressed:
        gremlin.control_action.cycle_modes(mode_list)

@chfs.button(12)
def switch_to_global(event):
    if event.is_pressed:
        gremlin.control_action.switch_mode("Global")

Precision Mode

This script switches to a lower sensitivity curve when any of the weapon groups are being fired and switches back to the default profile once no weapon is being fired any more. This is similar to the "sniper mode" that some gaming mice have, which drops the DPI setting at the press of a button. In this instance pressing the trigger automatically enables and disables this by switching the used response curve to one which halves the maximum response provided by the joystick at maximum deflection.

import gremlin
from gremlin.spline import CubicSpline
from vjoy.vjoy import AxisName

tm16000 = gremlin.input_devices.JoystickDecorator(
        "Thrustmaster T.16000M", 1325664945, "Global"
)

default_curve = CubicSpline(
        [(-1.0, -1.0), (0.0, 0.0), (1.0, 1.0)]
)
precision_curve = CubicSpline(
        [(-1.0, -0.5), (0.0, 0.0), (1.0, 0.5)]
)

active_weapon_groups = {}
active_curve = default_curve

def set_weapon_group(gid, is_pressed):
    global active_curve
    global active_weapon_groups
    if is_pressed:
        active_curve = precision_curve
        active_weapon_groups[gid] = True
    else:
        active_weapon_groups[gid] = False
        if sum(active_weapon_groups.values()) == 0:
            active_curve = default_curve

@tm16000.button(1)
def weapon_group_1(event, vjoy):
    set_weapon_group(1, event.is_pressed)
    vjoy[1].button(1).is_pressed = event.is_pressed

@tm16000.button(2)
def weapon_group_2(event, vjoy):
    set_weapon_group(2, event.is_pressed)
    vjoy[1].button(2).is_pressed = event.is_pressed

@tm16000.button(3)
def weapon_group_3(event, vjoy):
    set_weapon_group(3, event.is_pressed)
    vjoy[1].button(3).is_pressed = event.is_pressed

@tm16000.axis(1)
def pitch(event, vjoy):
    vjoy[1].axis(AxisName.X).value = active_curve(event.value)

@tm16000.axis(2)
def yaw(event, vjoy):
    vjoy[1].axis(AxisName.Y).value = active_curve(event.value)