From a58c2d919b479792409cf740567e166eedd4258b Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Thu, 24 Oct 2024 16:05:27 +0200 Subject: [PATCH 1/2] 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 a26724e3f3aeb31b8e907663b7538fe7ee3420cc Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Thu, 24 Oct 2024 16:04:55 +0200 Subject: [PATCH 2/2] Add FFB callback functions --- _sdk.py | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++ constants.py | 63 ++++++++++++++ vjoydevice.py | 35 +++++++- 3 files changed, 329 insertions(+), 1 deletion(-) diff --git a/_sdk.py b/_sdk.py index a9791c5..4592f96 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,234 @@ 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] + +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..f6b0494 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,37 @@ 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) + + @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""" + + packetnames = [None,"effect","envelope","cond","period","const","ramp","custom","sample",None,"effop","blkfree","ctrl","gain","setcustom",None,"neweff","blkload","pool"] + if reptype >= len(packetnames): + return None,0 + + typename = packetnames[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 \ No newline at end of file