WhiteMagic / JoystickGremlin

A tool for configuring and managing joystick devices.
http://whitemagic.github.io/JoystickGremlin/
GNU General Public License v3.0
313 stars 46 forks source link

SpaceMouse Axis Centering #107

Open spadino opened 6 years ago

spadino commented 6 years ago

Not a real issue, more like an ask for help.

I''m working with the last R11, trying to configure my 3D Connexion SpaceMouse. I wish to rely on the least possible tools to remap my whole devices, and as far as it seems, JoystickGremlin is the tool to go for, as it has potential to solve all the needs. Oh, I play Elite Dangerous...

By now, I have both my SM (SpaceMouse, by now) and G13 remapped; the first one the vJoy #1 (headlokk), the second to vJoy #2 (ship 6 axis). The G13 axis return always to the center, as expected, however the SM, even if I do not press it anymore, remain at the last position, so the axis never return to they neutral position. That's a real mess to fly with...

There's an already implemented method to map them the way I expected, returning to zero, or I have to write a custom module for that?

spadino commented 6 years ago

I found an old script in the closed issues, which I had to modify. Whenever I try to run in, it return me that error: image

import gremlin

# Device decorator
spacemouse = gremlin.input_devices.JoystickDecorator("SpaceMouse Pro", 74303019 , "Default")

# Store the last reported values of each axis for delta value computations
g_last_values = [0, 0, 0, 0, 0, 0, 0]

# Compute delta and set vjoy axis values
def process_event(axis_id, value, vjoy):
    global g_last_values
    delta = value - g_last_values[axis_id]

    # If the delta is too large, due to overflow we simply ignore the event
    if abs(delta) > 0.5:
        return

    # Limit the delta value to the range [-0.1, 0.1] as this is what the
    # device seems to adhere to range wise
    delta = min(0.1, max(-0.1, delta))

    # Set the axis value, multiplying by 10 to get into the range [-1, 1]
    # which is what normally is expected by Gremlin
    vjoy[3].axis(axis_id).value = delta * 10.0

    # Store the latest value for the next iteration
    g_last_values[axis_id] = value

# Callbacks for the individual axis of the device
@spacemouse.axis(1)
def axis1(event, vjoy):
    process_event(1, event.value, vjoy)

@spacemouse.axis(2)
def axis2(event, vjoy):
    process_event(2, event.value, vjoy)

@spacemouse.axis(3)
def axis3(event, vjoy):
    process_event(3, event.value, vjoy)

@spacemouse.axis(4)
def axis4(event, vjoy):
    process_event(4, event.value, vjoy)

@spacemouse.axis(5)
def axis5(event, vjoy):
    process_event(5, event.value, vjoy)

@spacemouse.axis(6)
def axis6(event, vjoy):
    process_event(6, event.value, vjoy)
WhiteMagic commented 6 years ago

From what I remember working on that script trying to remap the space mouse directly is super messy because it seems to accumulate changes, making it not usable directly. I'll have a look at that script and fix it up for the current version of Gremlin. That should be quite simple given I probably will only have to change a few lines where I've changed how things behave.

Another avenue would be to look at sx2joy which is talked about here #77. That tool directly reads the space mouse data and transforms it to axis values which are sent to a vJoy device. One of the features added to the current pre release of R11 is the ability to configure one vJoy device as a physical input. Mainly to allow the use of that software and similar with Gremlin. I have not tried this as I don't have a space mouse and the "vJoy as physical input" feature is quite new so there might be bugs. Also that would be one more program to run which you wanted to avoid.

In any case I'll fix that script up and post the corrected version here.

spadino commented 6 years ago

Thanks!

WhiteMagic commented 6 years ago

Ok I've gone over the code and it actually runs fine. I didn't look at the error and your information closely enough at the time. The cause for the error is that the script assumes that you have no duplicate devices (as far as their USB ids go).

Since you have two vJoy devices Gremlin is put into the mode where it assumes it has to handle multiple identical devices and starts to use usb id (hardware id) and windows id. So in your case to fix the script you have to replace this line: spacemouse = gremlin.input_devices.JoystickDecorator("SpaceMouse Pro", 74303019 , "Default") with this one spacemouse = gremlin.input_devices.JoystickDecorator("SpaceMouse Pro", (74303019, 1) , "Default") where the 1 in the brackets will have to be replaced with the windows id assigned to your space mouse, which you should be able to find via Tools -> Device Information.

spadino commented 6 years ago

Many thanks, @WhiteMagic! Very clever. I'll give it a try right now...

spadino commented 6 years ago

So, I had to make some modifications to the script, but now I got it to work. Now, unsatisfied - ;) - I want to proceed and implement an auto centering behavior, for the alternate mode I like to use. That's pretty different, as I like to have rotational axis also behaving in "mouse mode", but slowly returning to their centers if not too off-center. In FreePIE, I used a timer:

# Timer, for auto-centering
    system.setThreadTiming(TimingTypes.HighresSystemTimer)
    system.threadExecutionInterval = 5 # loop delay

Then, for calculate the axis, somethign like that:

# Smart auto-center parameters
global smart_radius, smart_speed
smart_radius = 4096
smart_speed = 96

# pitch
if (smpARX > 0) and (smpARX < (smart_radius)):
    smpARX = smpARX - smart_speed
elif (smpARX > smart_radius) and (smpARX < (smart_radius * 2)):
    smpARX = smpARX - (smart_speed / 2)
elif (smpARX > (smart_radius * 2)) and (smpARX < (smart_radius * 3)):
    smpARX = smpARX - (smart_speed / 4)
elif (smpARX < 0) and (smpARX > (smart_radius)*-1):
    smpARX = smpARX + smart_speed
elif (smpARX < (smart_radius) * -1) and (smpARX > (smart_radius) * -2):
    smpARX = smpARX + (smart_speed / 2)
elif (smpARX < (smart_radius) * -2) and (smpARX > (smart_radius) * -3):
    smpARX = smpARX + (smart_speed / 4)

There's some ways to process the axis in a similar fashion in Joystick Gremlin?

WhiteMagic commented 6 years ago

Gremlin has a decorator similar to the one used for devices that is intended for functions that should run periodically while Gremlin is active. A simple example is:

@gremlin.input_devices.periodic(0.25)
def recurring(vjoy, joy):
    vjoy[1].axis(1).value = joy[1].axis(1).value

This function will run every 0.25 seconds and write the value of the first axis of the first joystick (by windows id order) to the first axis of the first vJoy device. So this probably could serve as a starting point for reimplementing the FreePie version you have.

One thing to be aware of is that in Gremlin all axis values are between -1 and 1 as the values reported by devices all get normalized to that range independent of the actual precision their data is reported at. So if your code makes assumptions about value ranges you'll have to take that into account.

spadino commented 6 years ago

Thanks, @WhiteMagic! I saw that this was also on the documentation, so... sorry to had bothered you with that!

I got many thinks already implemented with Joystick Gremlin. So far, I'm delighted! I haven't proceed with the timer smart-centering, but I'll try to follow your tip. I noticed that the axis are normalized in range from -1 to 1. That's awesome! Very comfortable to work with... ;)

I was already used to a parallel multiple devices, which I map internally in the game mode, and I opted to follow the same path, so they seamless integrate into the game (not need a mode switch in gremlin, so far...). This is my current script:

import gremlin

###> Device decorator
spacemouse = gremlin.input_devices.JoystickDecorator("SpaceMouse Pro", (74303019, 3) , "Default")
#<

###> Parameters
# Flight models
last_values = [0, 0, 0, 0, 0, 0, 0]
relative_sens = 93 # Joystick alike
absolute_sens = 20 # Mouse alike
landing_ratio = 0.6 # provide a reduced active range, for delicate and precise movements

# Driving model
steering_sens = 15
throttle_sens = 30
#<
###<

###> Definitions
#> Relative axis 
def relative_mode(axis_id, value, vjoy):
    global last_values, relative_sens
    delta = value - last_values[axis_id]

    # Set the delta range
    delta = min(0.1, max(-0.1, delta))

    vjoy[1].axis(axis_id).value = delta * relative_sens

    # Store the latest value for the next iteration
    last_values[axis_id] = value
#<

#> Absolute axis
def absolute_mode(axis_id, value, vjoy):
    global absolute_sens, inc

    delta = vjoy[1].axis(axis_id).value / absolute_sens

    vjoy[3].axis(axis_id).value += delta
#< 

#> Landing axis
def landing_mode(axis_id, value, vjoy):
    global absolute_sens, inc

    delta = vjoy[1].axis(axis_id).value * landing_ratio

    vjoy[2].axis(axis_id).value = delta
#< 

#> Buggy Steering
def buggy_steering(axis_id, value, vjoy, axis_out):
    global steering_sens, inc

    delta = vjoy[1].axis(axis_id).value / steering_sens

    vjoy[4].axis(axis_out).value += delta
#<

#> Buggy Throttle
def buggy_throttle(axis_id, value, vjoy, axis_out):
    global throttle_sens, inc

    delta = vjoy[1].axis(axis_id).value / throttle_sens

    vjoy[4].axis(axis_out).value += delta
#<
###<

###> Callbacks for the individual axis of the device
@spacemouse.axis(1)
def axis1(event, vjoy):
    relative_mode(1, event.value, vjoy)
    landing_mode(1, event.value, vjoy)

@spacemouse.axis(2)
def axis2(event, vjoy):
    relative_mode(2, event.value, vjoy)
    landing_mode(2, event.value, vjoy)
    buggy_throttle(2, event.value, vjoy, 7)

@spacemouse.axis(3)
def axis3(event, vjoy):
    relative_mode(3, event.value, vjoy)
    landing_mode(3, event.value, vjoy)

@spacemouse.axis(4)
def axis4(event, vjoy):
    relative_mode(4, event.value, vjoy)
    landing_mode(4, event.value, vjoy)
    absolute_mode(4, event.value, vjoy)

@spacemouse.axis(5)
def axis5(event, vjoy):
    relative_mode(5, event.value, vjoy)
    landing_mode(5, event.value, vjoy)
    absolute_mode(5, event.value, vjoy)

@spacemouse.axis(6)
def axis6(event, vjoy):
    relative_mode(6, event.value, vjoy)
    landing_mode(6, event.value, vjoy)
    absolute_mode(6, event.value, vjoy)
    buggy_steering(6, event.value, vjoy, 8)
###<

It uses three parallels sets of axis for the SpaceMouse Pro, which can processed using various functions, as per need: one device has the full set of axis in relative movement; one device has the rotational axis processed for absolute (mouse alike) movement; a third device is for very sensitive operation (landing), where we want only a small range of action; and, finally, a couple of axis for the buggy (SRV), which provide a smooth throttle and steering. All axis can be, and most of them are, processed further through the outstanding Gremlin interface, receiving a custom response curve.

By now, I saw that sometime there are glitches, and axis get unresponsive. There's some way to have a more reliable operation? Can be something related to the R11 or is more related to the overall PC setup?

WhiteMagic commented 6 years ago

With regards to the glitches, hard to say without more information. Is there a particular pattern or symptom on how things bug out? There could definitely be bugs in Gremlin as I've never tried to continuously send this much stuff at various vjoy devices. If you haven't looked yet the system log Tools -> Log display may contain some information.

One thing in your script the use of the global keyword is only required when you intend to write to the variables after the keyword. If all you need is to read from the variable you don't have to do anything. It doesn't hurt the script though.

spadino commented 6 years ago

@WhiteMagic, I tweaked the priority of the processes, and it seems that there are no more glitches. I put Joystick Gremlin in Realtime and Elite Dangeorus in High Priority...

In regard my script, how can I define keyboard modifiers? I want to defien some keystrokes - for a throttle - but I need to use them with the left shift key.

If I use

# Increment
@gremlin.input_devices.keyboard("w", "shift", "Default")
def throttle_inc(event, mod, vjoy):
    if event.is_pressed and mod.is_pressed:
        mod_throttle(vjoy, -0.10)

it doen't error, but also do not work. If I use only the "w" key, it works, although... I tried shift, lshift and leftshift.

WhiteMagic commented 6 years ago

The keyboard decorator (the @ thing) only allows specifying a single key to listen for. This page https://whitemagic.github.io/JoystickGremlin/custom_modules/ attempts to go into a bit of detail but it's still rather abstract I fear. Decorators are a bit odd but in a sense they take a function mangle it a bit and return another function. If you want a really detailed explanation and probably some headaches this is a good read https://stackoverflow.com/a/1594484

A keyboard key decorator always takes two arguments, the name of the key and the mode name. Anything else is just noise it ignores. The function you're decorating gets at least the event passed in as the first parameter. You can add certain other specifically named parameters to get access to vJoy, joy, and keyboard device information. The decorator does some magic to ensure that if the function you write has vjoy in the parameter list that that variable will contain the vJoy object that allows to read and write to vJoy objects. Trying to pass arbitrary other parameters actually does nothing as the decorator doesn't know what to do with them.

So essentially to achieve what you're after you want to listen to a key press, say w and inside the function check for whether or not the left shift key is pressed as well. If those conditions are met you do whatever else you want to do. Modifying your above example this would look like this.

@gremlin.input_devices.keyboard("w", "Default")
def throttle_inc(event, keyboard):
    if event.is_pressed and keyboard.is_pressed("leftshift"):
        mod_throttle(vjoy, -0.10)

I've added the keyboard parameter to the parameter list of throttle_inc which makes the decorator magically populate that variable and I can ask the keyboard whether or not a key is pressed. To know what valid key names are you'll have to look at the bottom of https://github.com/WhiteMagic/JoystickGremlin/blob/develop/gremlin/macro.py which lists them.

The above function will only trigger if you first press shift and then w, if you reverse the order this won't set the throttle. You could work around this by not checking whether or not w is pressed , i.e. event.is_pressed, but then you run the risk of increasing the throttle twice if the key sequence is:

A solution around this is to have two functions, one for w and one for left shift. Each setting a boolean flag to true if they are pressed. When either function sees that after it did it's flag setting that both flags are true, it will execute the function. So something like this

import gremlin

is_w_pressed = False
is_shift_pressed = False

def throttle_inc(event):
    if is_w_pressed and is_shift_pressed:
        mod_throttle(vjoy, -0.10)

@gremlin.input_devices.keyboard("w", "Default")
def handle_w(event, vjoy):
    global is_w_pressed
    is_w_pressed = event.is_pressed
    throttle_inc(vjoy)

@gremlin.input_devices.keyboard("leftshift", "Default")
def handle_w(event, vjoy):
    global is_shift_pressed
    is_shift_pressed = event.is_pressed
    throttle_inc(vjoy)

Though this again has the drawback that if you hold down w and repeatedly press shift the throttle will increase :-) So there's always one more corner case to handle and it all depends on how fool proof you want to make it.

spadino commented 6 years ago

Thank you soooooo much for such claryfing and in depth reply. I'm so happy to had found you and your tool! I hope to get used to that new kind of behavior, so to contribute with snippets and, who knows, with the app development. All the best!