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.
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_type
identifier
device_guid
is_pressed
value
raw_value
From this list the only values that are typically of interest are
the is_pressed
and value
entries depending
on the input type.
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.
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)
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
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)
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.
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
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
FloatVariable
BoolVariable
StringVariable
ModeVariable
VirtualInputVariable
PhysicalInputVariable
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.
label
description
initial_value
min_value
max_value
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.
label
description
initial_value
min_value
max_value
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.
label
description
initial_value
This variable can hold any string and presents the user with a text input field.
label
description
initial_value
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.
label
description
This variable holds one specific vJoy input selection. The user is presented with the typical vJoy dropdown boxes.
label
description
valid_types
gremlin.common.InputType
valuesThis 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.
label
description
valid_types
gremlin.common.InputType
valuesThis 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)
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.
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.
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)
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)
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")
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)