4. User Plugins

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 user plugin. Each plugin is a simple Python script which defines the variables a user can configure via the UI and the functions (callbacks) triggered in reaction to (configured) 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 user plugin 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 user plugins is provided in Section 4.5. Finally, Section 4.6 provides a few practical examples.

4.1 Principles & Layout of User Plugin

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.

In order to make user plugins reusable and convenient to use a set of classes exist which allow setting them via the UI, thus allowing a user to customize the plugins directly from the UI. These variable classes allow configuration of commonly used types such as modes, inputs, as well as numerical values.

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.
device_guid
Unique device ID associated with 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. Indices of buttons and hats start at 1. For axes the indices correspond to the axis index as defined by the device. 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[device_guid]

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[device_guid].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[device_guid].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[device_guid].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 guid>}",
    "<mode>"
)

The value of device_guid is the unique dentifier of the device. 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 Configurable Variables

In order to allow user plugins to be configured by users via the UI several types of variable classes exist, that are automatically extracted from the plugin and presented to the user for customization.

The following variable types exist and will be explained in more detail below.

IntegerVariable
Holds a single integer value.
FloatVariable
Holds a single float value.
BoolVariable
Holds a single boolean value.
StringVariable
Holds an arbitrary length string.
ModeVariable
Holds the value of one of the modes existing in the profile.
VirtualInputVariable
Holds one vJoy selection.
PhysicalInputVariable
Holds one physical device input.

IntegerVariable

This variable can hold any single integer value and presents the user with a field which allows entering of values. The variable also allows the specification of limits for valid values.

gremlin.user_plugin.IntegerVariable.__init__
label
mandatory
Label shown for the UI element.
description
mandatory
Text describing the purpose of the variable.
initial_value
optional
Default value to use for this variable.
min_value
optional
Minimum value this variable can take on.
max_value
optional
Maximum value this variable can take on.

FloatVariable

This variable can hold any single floating point value and presents the user with a field which allows entering of values. The variable also allows the specification of limits for valid values.

gremlin.user_plugin.FloatVariable.__init__
label
mandatory
Label shown for the UI element.
description
mandatory
Text describing the purpose of the variable.
initial_value
optional
Default value to use for this variable.
min_value
optional
Minimum value this variable can take on.
max_value
optional
Maximum value this variable can take on.

BoolVariable

This variable can hold any single boolean value and presents the user with a checkbox. This can be used to turn features of a plugin on and off.

gremlin.user_plugin.BoolVariable.__init__
label
mandatory
Label shown for the UI element.
description
mandatory
Text describing the purpose of the variable.
initial_value
optional
Default value to use for this variable.

StringVariable

This variable can hold any string and presents the user with a text input field.

gremlin.user_plugin.StringVariable.__init__
label
mandatory
Label shown for the UI element.
description
mandatory
Text describing the purpose of the variable.
initial_value
optional
Default value to use for this variable.

ModeVariable

This variable holds the name of one mode present in this profile. The user is presented with a dropdown list containing all modes that exist.

gremlin.user_plugin.ModeVariable.__init__
label
mandatory
Label shown for the UI element.
description
mandatory
Text describing the purpose of the variable.

VirtualInputariable

This variable holds one specific vJoy input selection. The user is presented with the typical vJoy dropdown boxes.

gremlin.user_plugin.VirtualInputVariable.__init__
label
mandatory
Label shown for the UI element.
description
mandatory
Text describing the purpose of the variable.
valid_types
optional
List of valied gremlin.common.InputType values

PhysicalInputariable

This variable holds one specific physical input device selection. The user can press a button at which point Gremlin will record the physical input being activated next.

gremlin.user_plugin.PhysicalInputVariable.__init__
label
mandatory
Label shown for the UI element.
description
mandatory
Text describing the purpose of the variable.
valid_types
optional
List of valied gremlin.common.InputType values

Example

This example plugin lets a user specify four physical joystick buttons and map them to a single virtual hat output.

import gremlin
from gremlin.user_plugin import *


mode = ModeVariable(
        "Mode",
        "The mode to use for this mapping"
)
vjoy_hat = VirtualInputVariable(
        "Output Hat",
        "vJoy hat to use as the output",
        [gremlin.common.InputType.JoystickHat]
)
btn_1 = PhysicalInputVariable(
        "Button Up",
        "Button which will be mapped to the up direction of the hat.",
        [gremlin.common.InputType.JoystickButton]
)
btn_2 = PhysicalInputVariable(
        "Button Right",
        "Button which will be mapped to the right direction of the hat.",
        [gremlin.common.InputType.JoystickButton]
)
btn_3 = PhysicalInputVariable(
        "Button Down",
        "Button which will be mapped to the down direction of the hat.",
        [gremlin.common.InputType.JoystickButton]
)
btn_4 = PhysicalInputVariable(
        "Button Left",
        "Button which will be mapped to the left direction of the hat.",
        [gremlin.common.InputType.JoystickButton]
)

state = [0, 0]

decorator_1 = btn_1.create_decorator(mode.value)
decorator_2 = btn_2.create_decorator(mode.value)
decorator_3 = btn_3.create_decorator(mode.value)
decorator_4 = btn_4.create_decorator(mode.value)


def set_state(vjoy):
    device = vjoy[vjoy_hat.value["device_id"]]
    device.hat(vjoy_hat.value["input_id"]).direction  = tuple(state)


@decorator_1.button(btn_1.input_id)
def button_1(event, vjoy):
    global state
    state[1] = 1 if event.is_pressed else 0
    set_state(vjoy)


@decorator_2.button(btn_2.input_id)
def button_2(event, vjoy):
    global state
    state[0] = 1 if event.is_pressed else 0
    set_state(vjoy)


@decorator_3.button(btn_3.input_id)
def button_3(event, vjoy):
    global state
    state[1] = -1 if event.is_pressed else 0
    set_state(vjoy)


@decorator_4.button(btn_4.input_id)
def button_4(event, vjoy):
    global state
    state[0] = -1 if event.is_pressed else 0
    set_state(vjoy)

4.6 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.7 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)