From a58c2d919b479792409cf740567e166eedd4258b Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Thu, 24 Oct 2024 16:05:27 +0200 Subject: [PATCH 1/4] Remove duplicated SetBtn function --- _sdk.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/_sdk.py b/_sdk.py index 95e80e2..a9791c5 100755 --- a/_sdk.py +++ b/_sdk.py @@ -116,15 +116,6 @@ def SetContPov(PovValue, rID, PovID): -def SetBtn(state,rID,buttonID): - """Sets the state of vJoy Button to on or off. SetBtn(state,rID,buttonID)""" - result = _vj.SetBtn(state,rID,buttonID) - if result == 0: - raise vJoyButtonException() - else: - return True - - def ResetVJD(rID): """Reset all axes and buttons to default for specified vJoy Device""" return _vj.ResetVJD(rID) From 1c0348e75ff3327717c79d4bd53a6da0d43f9ff5 Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Thu, 24 Oct 2024 16:04:55 +0200 Subject: [PATCH 2/4] Add FFB callback functions --- README | 21 ++++- __init__.py | 2 +- _sdk.py | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++ constants.py | 63 ++++++++++++++ vjoydevice.py | 164 ++++++++++++++++++++++++++++++++++- 5 files changed, 481 insertions(+), 3 deletions(-) diff --git a/README b/README index 957fda3..123904b 100644 --- a/README +++ b/README @@ -3,7 +3,8 @@ pyvjoy is a set of Python bindings for vJoy (vjoystick.sourceforge.net) With this library you can easily set Axis and Button values on any vJoy device. Low-level bindings are provided in pyvjoy._sdk as well as a (hopefully) slightly more 'Pythonic' API in the pyvjoy.VJoyDevice() object. -Currently vJoyInterface.dll is looked for inside the pyvjoy directory only so place the desired version of that file there to use. (Note: this library currently only works with the x86 dll!) +Currently vJoyInterface.dll is looked for inside the pyvjoy directory only so place the desired version of that file there to use. +Note: FFB support requires a 2.2.x version for the effect block index to work properly. "Pointy's Joystick Test App" is very useful when testing vJoy and this library: http://www.planetpointy.co.uk/joystick-test-application/ @@ -52,6 +53,24 @@ j.data.wAxisY= 0x7500 j.update() +# FFB + +# Use effect manager helper +class EffMan(pyvjoy.FFB_Effect_Manager): + def update_effect_dict_cb(self,packetdict,idx): + self.print_effect(self.effects[idx]) # Print current internal effect state + + def update_ctrl_cb(self,ctrl): # 1 = enable, 2 = disable + print("Control",ctrl) + + def update_constant_cb(self,data,idx): # Constant force magnitude + print("CF",data["Magnitude"]) + + +if j.ffb_supported(): + effectManager1 = EffMan() + effectManager1.ffb_register_callback(j) + #Lower-level API just wraps the functions in the DLL as thinly as possible, with some attempt to raise exceptions instead of return codes. diff --git a/__init__.py b/__init__.py index b4417dc..a2ea78f 100755 --- a/__init__.py +++ b/__init__.py @@ -5,4 +5,4 @@ import pyvjoy._sdk -from pyvjoy.vjoydevice import VJoyDevice +from pyvjoy.vjoydevice import VJoyDevice, FFB_Effect_Manager diff --git a/_sdk.py b/_sdk.py index a9791c5..943888e 100755 --- a/_sdk.py +++ b/_sdk.py @@ -66,6 +66,7 @@ def RelinquishVJD(rID): if result == 0: raise vJoyFailedToRelinquishException() else: + FfbRemoveCB(rID) # Also delete FFB callback if present return True @@ -184,3 +185,236 @@ def set_defaults(self, rID): +# FFB: +class PacketStruct(Structure): + def to_dict(self): + return dict((field, getattr(self, field)) for field, _ in self._fields_ if field) + def keys(self): + return [field for field, _ in self._fields_ if field] + def __getitem__(self,key): + return getattr(self, key) + def values(self): + return [getattr(self, field) for field, _ in self._fields_ if field] + def __str__(self): + return str(self.to_dict()) + +class _FFB_DATA(Structure): + _fields_ = [ + ('size', c_ulong), + ('cmd', c_ulong), + ('data', c_void_p), + ] + +class _FFB_EFFECT(PacketStruct): + _pack_ = 1 + _fields_ = [ + ('EffectBlockIndex',c_uint32), + ('EffectType',c_uint32), + ('Duration',c_uint16), + ('TriggerRpt',c_uint16), + ('SamplePrd',c_uint16), + ('Gain',c_ubyte), + ('TriggerBtn',c_ubyte), + ('Polar',c_uint32), # Bool but 4 bytes padded + ('DirX',c_byte), # Polar direction or dirX depending on Polar bool + ('DirY',c_byte), + ] + + +class _FFB_EFF_RAMP(PacketStruct): + _pack_ = 1 + _fields_ = [ + ('EffectBlockIndex',c_uint32), + ('Start',c_int16), + ('',c_int16), # Reserved padding + ('End',c_int16), + ] + + +class _FFB_EFF_OP(PacketStruct): + _pack_ = 1 + _fields_ = [ + ('EffectBlockIndex',c_uint32), + ('EffectOp',c_uint32), + ('LoopCount',c_uint32), + ] + + +class _FFB_EFF_PERIOD(PacketStruct): + _pack_ = 1 + _fields_ = [ + ('EffectBlockIndex',c_uint32), + ('Magnitude',c_uint32), + ('Offset',c_int16), + ('',c_int16), # Padding + ('Phase',c_uint32), + ('Period',c_uint32), + ] + + +class _FFB_EFF_COND(PacketStruct): + _pack_ = 1 + _fields_ = [ + ('EffectBlockIndex',c_uint32), + ('isY',c_uint32), + ('CenterPointOffset',c_int16), + ('',c_int16), # Padding + ('PosCoeff',c_int16), + ('',c_int16), # Padding + ('NegCoeff',c_int16), + ('',c_int16), # Padding + ('PosSatur',c_uint32), + ('NegSatur',c_uint32), + ('DeadBand',c_int32), + ] + + +class _FFB_EFF_ENVLP(PacketStruct): + _pack_ = 1 + _fields_ = [ + ('EffectBlockIndex',c_uint32), + ('AttackLevel',c_uint32), + ('FadeLevel',c_uint32), + ('AttackTime',c_uint32), + ('FadeTime',c_uint32), + ] + + +class _FFB_EFF_CONST(PacketStruct): + _pack_ = 1 + _fields_ = [ + ('EffectBlockIndex',c_uint32), + ('Magnitude',c_int16), + ] + + +class FFBCallback(): + """Helper class for FFB callbacks between python and vjoy""" + vJoy_ffb_callback = None # Workaround to store callback functions + + def __init__(self): + self.callbacks = {} + self.internalcbtype = CFUNCTYPE(None,_FFB_DATA, c_void_p) + + # Callback can not be a member function + def ffbCallback(ffbpacket,userdata): + """Helper callback passed to vjoy. Will call previously registered python function with parsed FFB data""" + parsedData,reptype,devid = FFBCallback._parse_ffb_packet(ffbpacket) + if parsedData and (devid in self.callbacks): + # packet,typename = self.packet_to_dict(reptype,parsedData) + self.callbacks[devid](parsedData,reptype) + + self._internalcb = self.internalcbtype(ffbCallback) + + def addCallback(self,callback,rID): + """Add callback to rID device. Gets called when FFB data for rID is received""" + self.callbacks[rID] = callback + + def removeCallback(self,rID): + """Remove rID from internal callback dict""" + if rID in self.callbacks: + del self.callbacks[rID] + + @staticmethod + def _parse_ffb_packet(ffbpacket : _FFB_DATA): + """Helper function parse ffb data using vjoy functions""" + t = c_int(0) + res = _vj.Ffb_h_Type(ffbpacket, pointer(t)) + reptype = t.value + if res != 0: # Invalid packet + return None,0,0 + + devid = c_int(0) + _vj.Ffb_h_DeviceID(ffbpacket,pointer(devid)) # ID of vjoy device + + parsedPacket = None + + # Parse report type + if reptype == PT_CTRLREP: # Control rep + ctrl = c_int(0) + if _vj.Ffb_h_DevCtrl(ffbpacket,pointer(ctrl)) == 0: + parsedPacket = ctrl.value + + elif reptype == PT_EFFREP: # Set effect rep + tstruct = _FFB_EFFECT() + if _vj.Ffb_h_Eff_Report(ffbpacket,pointer(tstruct)) == 0: + parsedPacket = tstruct + + elif reptype == PT_RAMPREP: # Ramp rep + tstruct = _FFB_EFF_RAMP() + if _vj.Ffb_h_Eff_Ramp(ffbpacket,pointer(tstruct)) == 0: + parsedPacket = tstruct + + elif reptype == PT_EFOPREP: # EffOp rep + tstruct = _FFB_EFF_OP() + if _vj.Ffb_h_EffOp(ffbpacket,pointer(tstruct)) == 0: + parsedPacket = tstruct + + elif reptype == PT_PRIDREP: # EffPeriod rep + tstruct = _FFB_EFF_PERIOD() + if _vj.Ffb_h_Eff_Period(ffbpacket,pointer(tstruct)) == 0: + parsedPacket = tstruct + + elif reptype == PT_CONDREP: # Conditional rep + tstruct = _FFB_EFF_COND() + if _vj.Ffb_h_Eff_Cond(ffbpacket,pointer(tstruct)) == 0: + parsedPacket = tstruct + + elif reptype == PT_ENVREP: # Envelope rep + tstruct = _FFB_EFF_ENVLP() + if _vj.Ffb_h_Eff_Envlp(ffbpacket,pointer(tstruct)) == 0: + parsedPacket = tstruct + + elif reptype == PT_NEWEFREP: # NewEff rep + neweff = c_int(0) + if _vj.Ffb_h_EffNew(ffbpacket,pointer(neweff)) == 0: + parsedPacket = neweff.value + + elif reptype == PT_CONSTREP: # Constant force rep + tstruct = _FFB_EFF_CONST() + if _vj.Ffb_h_Eff_Constant(ffbpacket,pointer(tstruct)) == 0: + parsedPacket = tstruct + + elif reptype == PT_GAINREP: # Gain rep + gainrep = c_int(0) + if _vj.Ffb_h_DevGain(ffbpacket,pointer(gainrep)) == 0: + parsedPacket = gainrep.value + + elif reptype == PT_BLKFRREP: # Block free rep + blk = c_int(0) + if _vj.Ffb_h_EBI(ffbpacket,pointer(blk)) == 0: + parsedPacket = blk.value + + return parsedPacket,reptype,devid.value + + def getCcallback(self): + """Helper function returning the external C-type callback""" + return self._internalcb + +def FfbRegisterGenCB(func,rID): + """Registers a python FFB callback and translates packets""" + if not FFBCallback.vJoy_ffb_callback: + FFBCallback.vJoy_ffb_callback = FFBCallback() + + FFBCallback.vJoy_ffb_callback.addCallback(func,rID) + devid = c_int(rID) + _vj.FfbRegisterGenCB(FFBCallback.vJoy_ffb_callback.getCcallback(),pointer(devid)) + +def FfbRemoveCB(rID): + """Removes a callback from the helper class""" + if FFBCallback.vJoy_ffb_callback: + FFBCallback.vJoy_ffb_callback.removeCallback(rID) + +def vJoyFfbCap(): + """Returns True if vjoy is FFB capable""" + ret = c_bool(False) + _vj.vJoyFfbCap(pointer(ret)) + return ret.value + +def IsDeviceFfb(rID): + """Returns True if device is FFB capable""" + return _vj.IsDeviceFfb(rID) != 0 + +def IsDeviceFfbEffect(rID, effect): + """Returns True if device supports effect usage type""" + return _vj.IsDeviceFfbEffect(rID,effect) != 0 diff --git a/constants.py b/constants.py index 91c0dcc..15aa868 100755 --- a/constants.py +++ b/constants.py @@ -22,3 +22,66 @@ VJD_STAT_MISS = 3 # The vJoy Device is missing. It either does not exist or the driver is down. VJD_STAT_UNKN = 4 # Unknown +# FFB rep +PT_EFFREP = 0x01 # Usage Set Effect Report +PT_ENVREP = 0x02 # Usage Set Envelope Report +PT_CONDREP = 0x03 # Usage Set Condition Report +PT_PRIDREP = 0x04 # Usage Set Periodic Report +PT_CONSTREP = 0x05 # Usage Set Constant Force Report +PT_RAMPREP = 0x06 # Usage Set Ramp Force Report +PT_CSTMREP = 0x07 # Usage Custom Force Data Report +PT_SMPLREP = 0x08 # Usage Download Force Sample +PT_EFOPREP = 0x0A # Usage Effect Operation Report +PT_BLKFRREP = 0x0B # Usage PID Block Free Report +PT_CTRLREP = 0x0C # Usage PID Device Control +PT_GAINREP = 0x0D # Usage Device Gain Report +PT_SETCREP = 0x0E # Usage Set Custom Force Report + +# FFB feature rep +PT_NEWEFREP = 0x01+0x10 # Usage Create New Effect Report +PT_BLKLDREP = 0x02+0x10 # Usage Block Load Report +PT_POOLREP = 0x03+0x10 # Usage PID Pool Report + + +# Effect Type +ET_NONE = 0 # No Force +ET_CONST = 1 # Constant Force +ET_RAMP = 2 # Ramp +ET_SQR = 3 # Square +ET_SINE = 4 # Sine +ET_TRNGL = 5 # Triangle +ET_STUP = 6 # Sawtooth Up +ET_STDN = 7 # Sawtooth Down +ET_SPRNG = 8 # Spring +ET_DMPR = 9 # Damper +ET_INRT = 10 # Inertia +ET_FRCTN = 11 # Friction +ET_CSTM = 12 # Custom Force Data + + +# Effect operation +EFF_START = 1 # EFFECT START +EFF_SOLO = 2 # EFFECT SOLO START +EFF_STOP = 3 # EFFECT STOP + + +# FFB ctrl +CTRL_ENACT = 1 # Enable all device actuators. +CTRL_DISACT = 2 # Disable all the device actuators. +CTRL_STOPALL = 3 # Stop All Effects:­ Issues a stop on every running effect. +CTRL_DEVRST = 4 # Device Reset: Clears any device paused condition, enables all actuators and clears all effects from memory. +CTRL_DEVPAUSE = 5 # Pause: All effects on the device are paused at the current time step. +CTRL_DEVCONT = 6 # Device Continue: The all effects that running when the device was paused are restarted from their last time step. + +# HID effect usage types +HID_USAGE_CONST = 0x26 # Usage ET Constant Force +HID_USAGE_RAMP = 0x27 # Usage ET Ramp +HID_USAGE_SQUR = 0x30 # Usage ET Square +HID_USAGE_SINE = 0x31 # Usage ET Sine +HID_USAGE_TRNG = 0x32 # Usage ET Triangle +HID_USAGE_STUP = 0x33 # Usage ET Sawtooth Up +HID_USAGE_STDN = 0x34 # Usage ET Sawtooth Down +HID_USAGE_SPRNG = 0x40 # Usage ET Spring +HID_USAGE_DMPR = 0x41 # Usage ET Damper +HID_USAGE_INRT = 0x42 # Usage ET Inertia +HID_USAGE_FRIC = 0x43 # Usage ET Friction diff --git a/vjoydevice.py b/vjoydevice.py index 59de77a..fc356c9 100755 --- a/vjoydevice.py +++ b/vjoydevice.py @@ -13,7 +13,7 @@ def __init__(self,rID=None, data=None): self.rID=rID self._sdk=_sdk self._vj=self._sdk._vj - + if data: self.data = data else: @@ -74,4 +74,166 @@ def __del__(self): # free up the controller before losing access self._sdk.RelinquishVJD(self.rID) + + def ffb_supported(self): + """Returns True if device is FFB capable""" + return self._sdk.vJoyFfbCap() and self._sdk.IsDeviceFfb(self.rID) + + def ffb_effect_supported(self,effect): + """Returns True if device supports effect usage type""" + return self._sdk.IsDeviceFfbEffect(self.rID,effect) + + def ffb_register_callback(self,callback): + """Registers a callback for FFB data for this device""" + self._sdk.FfbRegisterGenCB(callback,self.rID) + + + +class FFB_Effect_Manager(): + """Helper class that stores the current state of all effects and handles callbacks""" + PACKET_TO_NAME = [None,"effect","envelope","cond","period","const","ramp","custom", + "sample",None,"effop","blkfree","ctrl","gain","setcustom",None, + "neweff","blkload","pool"] + EFFECTTYPE_TO_NAME = ["None","Const","Ramp","Square","Sine","Triangle","SawtoothUp", + "SawtoothDown","Spring","Damper","Inertia","Friction","Custom"] + + def __init__(self): + self.effects = [] # Effect storage. vjoy 2.2.x now supports multiple effect blocks + + def update_packet_cb(self,data,reptype,idx): + """Called after every ffb update to update internal dict. + Override to modify if internal state updating is not required""" + packetdict,ebi = FFB_Effect_Manager.ffb_packet_to_dict(data,reptype) + if len(self.effects) <= idx: # Extend effect storage + self.effects.extend([{} for _ in range(1+idx-len(self.effects) ) ] ) + + self.effects[idx].update(packetdict) + self.update_effect_dict_cb(packetdict,idx) + return + + def update_effect_dict_cb(self,packetdict,idx): + """Called after every ffb update with parsed dict by update_packet_cb""" + return + + def update_ctrl_cb(self,ctrl): + """Control packet callback. Value can be any CTRL_ constant""" + return + + def update_gain_cb(self,gain): + """Gain packet callback. Device gain 0-255""" + return + + def update_effect_op_cb(self,enabled,idx): + """Change effect state callback""" + return + + def update_effect_cb(self,data,idx): + """Set effect (effect) callback""" + return + + def update_envelope_cb(self,data,idx): + """Set envelope callback""" + return + + def update_condition_cb(self,data,idx): + """Set condition (condX or condY) callback""" + return + def update_periodic_cb(self,data,idx): + """Set periodic callback""" + return + + def update_constant_cb(self,data,idx): + """Set constant force callback""" + return + + def update_ramp_cb(self,data,idx): + """Set ramp callback""" + return + + + def __ffb_cb(self,data,reptype): + """Callback handling raw sdk data""" + if reptype == PT_BLKFRREP: # Delete block + self.effects[data-1].clear() + + if reptype == PT_CTRLREP: + if(data == CTRL_DEVRST): + self.effects.clear() # Reset clears all effects + elif(data == CTRL_STOPALL): + for e in self.effects: + if "effop" in e: + e["effop"]["EffectOp"] = EFF_STOP + self.update_effect_op_cb(False,e["effop"]["EffectBlockIndex"]) + + self.update_ctrl_cb(data) + + ebi = getattr(data,"EffectBlockIndex",0) + if ebi: # Packet specifies effect block index + self.update_packet_cb(data,reptype,ebi-1) + + if reptype == PT_EFOPREP: + self.update_effect_op_cb((data.EffectOp != EFF_STOP),ebi-1) + elif reptype == PT_EFFREP: + self.update_effect_cb(data,ebi-1) + elif reptype == PT_ENVREP: + self.update_envelope_cb(data,ebi-1) + elif reptype == PT_CONDREP: + self.update_condition_cb(data,ebi-1) + elif reptype == PT_PRIDREP: + self.update_periodic_cb(data,ebi-1) + elif reptype == PT_CONSTREP: + self.update_constant_cb(data,ebi-1) + elif reptype == PT_RAMPREP: + self.update_ramp_cb(data,ebi-1) + + elif reptype == PT_GAINREP: + self.update_gain_cb(data) + + def ffb_register_callback(self,j : VJoyDevice): + """Registers this class as the ffb callback for vjoy device j""" + j.ffb_register_callback(self.__ffb_cb) + + @staticmethod + def ffb_packet_to_dict(data,reptype : int): + """Helper function to convert FFB packets into named python dicts. + Returns dict with single named entry and effect block index if applicable. + Otherwise ebi is 0 for control reports""" + + if reptype >= len(FFB_Effect_Manager.PACKET_TO_NAME): + return None,0 + + typename = FFB_Effect_Manager.PACKET_TO_NAME[reptype] + if reptype == _sdk.PT_CONDREP: + typename += "Y" if data["isY"] else "X" + ebi = 0 + if isinstance(data,_sdk.PacketStruct): + data = data.to_dict() + if "EffectBlockIndex" in data: + ebi = data["EffectBlockIndex"] + ret = {typename:data} + return ret,ebi + + @staticmethod + def effect_get_state(effect : dict): + """Gets the current state of an effect dict. False if stopped, True otherwise""" + return effect.get("effop",{}).get("EffectOp",EFF_STOP) != EFF_STOP + + @staticmethod + def get_effect_name(effect : dict): + """Extracts effect name from effect dict""" + return FFB_Effect_Manager.EFFECTTYPE_TO_NAME[effect['effect'].get('EffectType',0)] + + @staticmethod + def print_effect(effect): + """Helper function to print a single effect dict""" + if effect and "effect" in effect: + effectname = FFB_Effect_Manager.get_effect_name(effect) + state = FFB_Effect_Manager.effect_get_state(effect) + print(f"Type: {effectname}, State: {state}, Data: {effect}") + + @staticmethod + def print_effects(effects): + """Helper function to print all current effect data""" + for effect in effects: + FFB_Effect_Manager.print_effect(effect) \ No newline at end of file From b902b81606b3f3855046b0d45d6ef49434ed8d95 Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Sun, 5 Jan 2025 21:48:41 +0100 Subject: [PATCH 3/4] Readme update --- README | 77 --------------------------------- README.md | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 77 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 123904b..0000000 --- a/README +++ /dev/null @@ -1,77 +0,0 @@ -pyvjoy is a set of Python bindings for vJoy (vjoystick.sourceforge.net) - -With this library you can easily set Axis and Button values on any vJoy device. -Low-level bindings are provided in pyvjoy._sdk as well as a (hopefully) slightly more 'Pythonic' API in the pyvjoy.VJoyDevice() object. - -Currently vJoyInterface.dll is looked for inside the pyvjoy directory only so place the desired version of that file there to use. -Note: FFB support requires a 2.2.x version for the effect block index to work properly. - -"Pointy's Joystick Test App" is very useful when testing vJoy and this library: http://www.planetpointy.co.uk/joystick-test-application/ - -USAGE ------ - -import pyvjoy - -#Pythonic API, item-at-a-time - -j = pyvjoy.VJoyDevice(1) - -#turn button number 15 on -j.set_button(15,1) - -#Notice the args are (buttonID,state) whereas vJoy's native API is the other way around. - - -#turn button 15 off again -j.set_button(15,0) - -#Set X axis to fully left -j.set_axis(pyvjoy.HID_USAGE_X, 0x1) - -#Set X axis to fully right -j.set_axis(pyvjoy.HID_USAGE_X, 0x8000) - -#Also implemented: - -j.reset() -j.reset_buttons() -j.reset_povs() - - -#The 'efficient' method as described in vJoy's docs - set multiple values at once - -j.data ->>> - - -j.data.lButtons = 19 # buttons number 1,2 and 5 (1+2+16) -j.data.wAxisX = 0x2000 -j.data.wAxisY= 0x7500 - -#send data to vJoy device -j.update() - - -# FFB - -# Use effect manager helper -class EffMan(pyvjoy.FFB_Effect_Manager): - def update_effect_dict_cb(self,packetdict,idx): - self.print_effect(self.effects[idx]) # Print current internal effect state - - def update_ctrl_cb(self,ctrl): # 1 = enable, 2 = disable - print("Control",ctrl) - - def update_constant_cb(self,data,idx): # Constant force magnitude - print("CF",data["Magnitude"]) - - -if j.ffb_supported(): - effectManager1 = EffMan() - effectManager1.ffb_register_callback(j) - -#Lower-level API just wraps the functions in the DLL as thinly as possible, with some attempt to raise exceptions instead of return codes. - - - diff --git a/README.md b/README.md new file mode 100644 index 0000000..95ac3d3 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +Forked from [tidzo/pyvjoy](https://github.com/tidzo/pyvjoy) to support FFB callbacks. + +Note: FFB support requires a 2.2.x version for the effect block index to work properly. +The original vJoy project on vjoystick.sourceforge is not being updated anymore and does not support FFB properly. + +It is recommended to use a fork like [BrunnerInnovation/vJoy](https://github.com/BrunnerInnovation/vJoy). + +The interface dll from this fork is being used for FFB. + +With this library you can easily set Axis and Button values on any vJoy device and receive force feedback effect data. +Low-level bindings are provided in pyvjoy._sdk as well as a (hopefully) slightly more 'Pythonic' API in the pyvjoy.VJoyDevice() object. + +The usage of non FFB functions is identical to the original pyvJoy project. + +## Standard pyvjoy usage: +```python +import pyvjoy + +#Pythonic API, item-at-a-time + +j = pyvjoy.VJoyDevice(1) + +#turn button number 15 on +j.set_button(15,1) + +#turn button 15 off again +j.set_button(15,0) + +#Set X axis to fully left +j.set_axis(pyvjoy.HID_USAGE_X, 0x1) + +#Set X axis to fully right +j.set_axis(pyvjoy.HID_USAGE_X, 0x8000) + +#Also implemented: +j.reset() +j.reset_buttons() +j.reset_povs() + +#The 'efficient' method as described in vJoy's docs - set multiple values at once + +j.data.lButtons = 19 # buttons number 1,2 and 5 (1+2+16) +j.data.wAxisX = 0x2000 +j.data.wAxisY= 0x7500 + +#send data to vJoy device +j.update() +``` +## Simple FFB example with callbacks: +```python +import pyvjoy +import time + +j = pyvjoy.VJoyDevice(1) +# FFB functions + +# Use effect manager helper to simplify effect handling +# idx (index in effect list) is blockindex-1 because blockindex always starts with 1. +# Effect events can be received using the separate callback functions or by overriding update_packet_cb, update_effect_dict_cb or __ffb_cb + +class EffMan(pyvjoy.FFB_Effect_Manager): + + def update_ctrl_cb(self,ctrl): # 1 = enable, 2 = disable + print("Control",ctrl) + + def update_constant_cb(self,data,idx): # Constant force magnitude + print("CF",data["Magnitude"]) + + # Also implemented (Function dummys from FFB_Effect_Manager class) + def update_gain_cb(self,gain): + """Gain packet callback. Device gain 0-255""" + return + + def update_effect_op_cb(self,enabled,idx): + """Change effect state callback""" + return + + def update_effect_cb(self,data,idx): + """Set effect (effect) callback""" + return + + def update_envelope_cb(self,data,idx): + """Set envelope callback""" + return + + def update_condition_cb(self,data,idx): + """Set condition (condX or condY) callback""" + return + + def update_periodic_cb(self,data,idx): + """Set periodic callback""" + return + + def update_ramp_cb(self,data,idx): + """Set ramp callback""" + return + + def update_effect_dict_cb(self,packetdict,idx): + return + + +if j.ffb_supported(): # Only if FFB is actually enabled in device and driver + effectManager1 = EffMan() + effectManager1.ffb_register_callback(j) +time.sleep(100) +print("End") +``` +## Low level minimal FFB example without creating the FFB_Effect_Manager: +```python +import pyvjoy +import time + +j = pyvjoy.VJoyDevice(1) + +def ffbcb(data,reptype): + packetdict,ebi = pyvjoy.FFB_Effect_Manager.ffb_packet_to_dict(data,reptype) + print(packetdict,ebi) + +j.ffb_register_callback(ffbcb) +time.sleep(100) +print("End") +``` + + From b1c99a9ee11e208e989e69f516cbb7c7ca0d1a8b Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Sun, 16 Feb 2025 22:15:19 +0100 Subject: [PATCH 4/4] _FFB_EFFECT Padding added to make dirX accessible --- _sdk.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/_sdk.py b/_sdk.py index 943888e..bad097c 100755 --- a/_sdk.py +++ b/_sdk.py @@ -209,15 +209,18 @@ class _FFB_EFFECT(PacketStruct): _pack_ = 1 _fields_ = [ ('EffectBlockIndex',c_uint32), - ('EffectType',c_uint32), + ('EffectType',c_uint), ('Duration',c_uint16), ('TriggerRpt',c_uint16), ('SamplePrd',c_uint16), + ('StartDelay',c_uint16), ('Gain',c_ubyte), ('TriggerBtn',c_ubyte), - ('Polar',c_uint32), # Bool but 4 bytes padded - ('DirX',c_byte), # Polar direction or dirX depending on Polar bool - ('DirY',c_byte), + ('Polar',c_ubyte), # Axes enable or direction enable (Bit 3) + ('',c_ubyte), # Reserved padding + ('',c_uint32), # Reserved padding TODO Seems to contain data + ('DirX',c_uint16), # Polar direction or dirX depending on Polar. + ('DirY',c_uint16), ]