WhiteMagic / JoystickGremlin

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

Repeat input faster and faster #475

Open therik opened 1 year ago

therik commented 1 year ago

Hello,

TL;DR: I need to terminate all instances of a currently running macro if an event wasn't emitted for the last quarter second or so. Or, alternatively (preferably), I need to be told that what I'm doing is ridiculous and there's an easier way to achieve this.

Long version:

I was trying to make a plugin that would implement something similar to how the rotary knob for autopilot works in 737.

The autopilot knobs in 737 are physically similar in function to a mouse scroll wheel, it's a knob that can turn in individual steps, full 360 degrees endlessly. It's a button-ish device, not an axis - each step of rotation emits an event.

Logically though, each click of rotation in the 737 increases a value (airspeed, heading, whatever) by one at first, slow rotation of the knob continues to increase the value in increments of one, but the faster the pilot turns that knob, the bigger the increments become.

IE. fast rotation of the knob increases the value by increments that are bigger than one - faster one rotates the clicky knob, the bigger the increments, thus the rotation speed to value increase is non-linear.

What I'm trying to achieve is something similar, but in gremlin, so that I can enjoy the same functionality in different games or planes that don't have such a clever knob.

I'm trying to figure out the speed of rotation (clicks per second) and emmit a correspondingly strong output from joystickgremlin.

I have a physical controller that has a rotary knob which emits a quick pair of press+release events on each rotation click (I think it's 20ms by default, but it's somewhat configurable in the driver).

I wrote a plugin that remembers how many events were emmitted by the hardware in the last second, and based on that it would determine how much should the in-game number change - that part is fine.

The issue:

The problem I'm having is that the in-game dial can only be bound to a button, and on top of that it doesn't move one value per click of a button, it moves certain amount of values per second while holding the button.

My idea was to emit a virtual, short (let's say the same 20ms as comes from the hardware) press+release pair of events for each click of the physical dial knob, and as the user would spin the dial knob faster, gremlin would still emit one event per click, but the length of the events emited by gremlin would increase to 30, 50 or more miliseconds between press and release of the virtual button, until it all becomes a continuously held down virtual button. That continuously pressed virtual button would stay pressed until the user stops scrolling the physical knob or slows down to the point that the virtual gremlin output would be individual "dashes" again.

But I don't know how to schedule a button release event in any other way except by defining a macro, and the number of macros quickly grew to the point that the in-game knob was scrolling long after I stopped scrolling the physical knob - even when macro was set as exclusive.

On top of that, MacroManager.terminate_macro didn't seem to actually terminate the macro when I tried that.

Is there some other way of scheduling events in gremlin plugins?

Code

I'm gonna dump the plugin that I wrote here, it's ofcourse just a proof of concept, it should send the short press+release events initially just as they come from the hardware, but if there was more than 7 clicks in the last second, it should schedule a 200ms long press+release event through a macro. But it doesn't work, if the clicks are faster than every 200ms, too many macros get scheduled and the in-game knob then keeps rotating for a while until all the macros get consumed.

Can you give me some pointers please? It feels like the way I'm doing it is not the way it's meant to be done through macros.

import gremlin
from vjoy.vjoy import AxisName
import time

joystick_decorator = gremlin.input_devices.JoystickDecorator(
    "LEFT VPC Throttle MT-50CM3",
    "{9CEE0190-0D28-11ED-8001-444553540000}",
    "Default"
)

onAction = gremlin.macro.VJoyAction(1, gremlin.common.InputType.JoystickButton, 3, True)
offAction = gremlin.macro.VJoyAction(1, gremlin.common.InputType.JoystickButton, 3, False)

timetable = []
def _addToTimetable():
    global timetable
    t = int(time.time() * 1000)
    cutoff = t - 1000
    timetable = list(filter(lambda elem: elem > cutoff, timetable))
    timetable.append(t)
#    gremlin.util.log(len(timetable))

onMacro = gremlin.macro.Macro()
onMacro.add_action(onAction)
onMacro.pause(0.2)

offMacro = gremlin.macro.Macro()
offMacro.add_action(offAction)

@joystick_decorator.button(119)
def apRepeatCCW(event, vjoy):
    if event.is_pressed:
        _addToTimetable()

    if len(timetable) > 7:
        if event.is_pressed:
             gremlin.macro.MacroManager().queue_macro(onMacro)
             gremlin.macro.MacroManager().queue_macro(offMacro)
    else:
        vjoy[1].button(3).is_pressed = event.is_pressed
WhiteMagic commented 1 year ago

Attempting to interrupt macros via terminate_macro is futile as it won't do that. All it does is remove macros that are repeatedly running, i.e. repeat, toggle, hold, etc. from the queue of Macros. Interrupting a running macro is not supported, one reason being that it could lead to the entire system being in a messed up state, say keys being held down etc.

Regarding your code, the way you send the macros would execute both the onMacro and offMacro simultaneously, which from your description, isn't what you're after.

For your use case, I don't think using pauses inside Macros is useful, as the pause is what you want to play with, I suspect. From my understanding, you need to hold down the button for some time which is in line with how fast the wheel is scrolled. If you can compute how long the button ought to be held down you should be able to use the Timer class from Python's threading package to execute the button release when needed. As you continue scrolling, you'd have to continuously cancel and recreate it to be in line with the new release time. Alternatively, you could use a thread that checks these things, but that would need more fiddling to ensure it is happy and you'd need to figure out when to remove it etc. so it's not checking at a high rate for no reason.

The bigger conceptual issue I see though is that if you need to do bigger steps the faster you scroll but the input only does fixed-size increments you will eventually "outrun" the increments and the time of release will be after you physically let go of the button.