Python bindings to the Linux input handling subsystem for reading input events and creating virtual input devices
—
Support for force feedback effects on compatible devices. The ff module provides ctypes structures and utilities for creating, uploading, and managing haptic feedback effects like rumble, springs, and custom waveforms.
Core ctypes structures for defining force feedback effects.
class Effect(ctypes.Structure):
"""
Main force feedback effect structure.
Fields:
- type: Effect type (FF_RUMBLE, FF_PERIODIC, etc.)
- id: Effect identifier (set by kernel)
- direction: Effect direction (0x0000 to 0xFFFF)
- ff_trigger: Trigger conditions (Trigger structure)
- ff_replay: Scheduling information (Replay structure)
- u: Union of effect-specific data (EffectType)
"""
class EffectType(ctypes.Union):
"""
Union containing effect-specific parameters.
Fields:
- constant: Constant force effect data
- ramp: Ramp effect data
- periodic: Periodic effect data
- condition: Condition effect data (spring, friction, etc.)
- rumble: Rumble effect data
"""Effect type union usage patterns:
# Access different effect types through union
effect.u.rumble.strong_magnitude = 0x8000 # For rumble effects
effect.u.constant.level = 0x4000 # For constant effects
effect.u.periodic.waveform = ecodes.FF_SINE # For periodic effects
effect.u.condition[0].right_coeff = 0x2000 # For condition effects (array)Structures for controlling when and how effects are played.
class Replay(ctypes.Structure):
"""
Effect scheduling and timing control.
Fields:
- length: Duration of effect in milliseconds (0 = infinite)
- delay: Delay before effect starts in milliseconds
"""
class Trigger(ctypes.Structure):
"""
Effect trigger conditions.
Fields:
- button: Button number that triggers effect (0 = no trigger)
- interval: Minimum time between triggers in milliseconds
"""
class Envelope(ctypes.Structure):
"""
Effect envelope for attack/fade control.
Fields:
- attack_length: Duration of attack phase in milliseconds
- attack_level: Starting amplitude level (0x0000-0x7FFF)
- fade_length: Duration of fade phase in milliseconds
- fade_level: Ending amplitude level (0x0000-0x7FFF)
"""Specific effect structures for different force feedback types.
class Rumble(ctypes.Structure):
"""
Rumble/vibration effect.
Fields:
- strong_magnitude: Strong motor magnitude (0x0000-0xFFFF)
- weak_magnitude: Weak motor magnitude (0x0000-0xFFFF)
"""
class Constant(ctypes.Structure):
"""
Constant force effect.
Fields:
- level: Force level (-0x7FFF to 0x7FFF)
- ff_envelope: Envelope for attack/fade (Envelope structure)
"""
class Ramp(ctypes.Structure):
"""
Force ramp effect (force changes linearly over time).
Fields:
- start_level: Starting force level (-0x7FFF to 0x7FFF)
- end_level: Ending force level (-0x7FFF to 0x7FFF)
- ff_envelope: Envelope for attack/fade (Envelope structure)
"""
class Periodic(ctypes.Structure):
"""
Periodic waveform effect.
Fields:
- waveform: Waveform type (FF_SQUARE, FF_SINE, etc.)
- period: Period of waveform in milliseconds
- magnitude: Peak magnitude (0x0000-0x7FFF)
- offset: DC offset (-0x7FFF to 0x7FFF)
- phase: Phase offset (0x0000-0x7FFF, 0=0°, 0x4000=90°)
- envelope: Envelope structure
- custom_len: Length of custom waveform data
- custom_data: Pointer to custom waveform data
"""
class Condition(ctypes.Structure):
"""
Condition effects (spring, friction, damper, inertia).
Fields:
- right_saturation: Right saturation level (0x0000-0xFFFF)
- left_saturation: Left saturation level (0x0000-0xFFFF)
- right_coeff: Right coefficient (-0x7FFF to 0x7FFF)
- left_coeff: Left coefficient (-0x7FFF to 0x7FFF)
- deadband: Deadband size (0x0000-0xFFFF)
- center: Center position (-0x7FFF to 0x7FFF)
"""Structures for managing force feedback effects on virtual devices.
class UInputUpload(ctypes.Structure):
"""
Structure for uploading effects to UInput devices.
Fields:
- request_id: Request identifier
- retval: Return value/error code
- effect: Effect structure to upload
- old: Previous effect data
"""
class UInputErase(ctypes.Structure):
"""
Structure for erasing effects from UInput devices.
Fields:
- request_id: Request identifier
- retval: Return value/error code
- effect_id: Effect ID to erase
"""from evdev import InputDevice, UInput, ecodes
from evdev.ff import Effect, Rumble, Replay, Trigger
import ctypes
import time
# Create or open device with force feedback capability
device = InputDevice('/dev/input/event0') # Must support FF
# Check if device supports force feedback
caps = device.capabilities()
if ecodes.EV_FF not in caps:
print("Device does not support force feedback")
device.close()
exit()
# Create rumble effect
effect = Effect()
effect.type = ecodes.FF_RUMBLE
effect.id = -1 # Let kernel assign ID
effect.direction = 0x0000 # Direction (not used for rumble)
# Set up timing
effect.ff_replay.length = 1000 # 1 second duration
effect.ff_replay.delay = 0 # No delay
# Set up trigger (optional)
effect.ff_trigger.button = 0 # No button trigger
effect.ff_trigger.interval = 0 # No interval
# Configure rumble parameters
effect.u.rumble.strong_magnitude = 0x8000 # 50% strong motor
effect.u.rumble.weak_magnitude = 0x4000 # 25% weak motor
try:
# Upload effect to device
effect_id = device.upload_effect(effect)
print(f"Effect uploaded with ID: {effect_id}")
# Play the effect
device.write(ecodes.EV_FF, effect_id, 1) # Start effect
device.syn()
time.sleep(1.5) # Wait for effect to complete
# Stop effect (if still playing)
device.write(ecodes.EV_FF, effect_id, 0) # Stop effect
device.syn()
# Remove effect from device
device.erase_effect(effect_id)
print("Effect erased")
finally:
device.close()from evdev import InputDevice, ecodes
from evdev.ff import Effect, Periodic, Replay, Envelope
import ctypes
import time
device = InputDevice('/dev/input/event0')
# Create periodic sine wave effect
effect = Effect()
effect.type = ecodes.FF_PERIODIC
effect.id = -1
effect.direction = 0x4000 # 90 degrees
# Timing
effect.ff_replay.length = 2000 # 2 seconds
effect.ff_replay.delay = 0
# No trigger
effect.ff_trigger.button = 0
effect.ff_trigger.interval = 0
# Periodic parameters
effect.u.periodic.waveform = ecodes.FF_SINE
effect.u.periodic.period = 100 # 100ms period (10 Hz)
effect.u.periodic.magnitude = 0x6000 # 75% magnitude
effect.u.periodic.offset = 0 # No DC offset
effect.u.periodic.phase = 0 # 0° phase
# Envelope for smooth start/end
effect.u.periodic.envelope.attack_length = 200 # 200ms attack
effect.u.periodic.envelope.attack_level = 0x0000
effect.u.periodic.envelope.fade_length = 200 # 200ms fade
effect.u.periodic.envelope.fade_level = 0x0000
try:
effect_id = device.upload_effect(effect)
# Play effect
device.write(ecodes.EV_FF, effect_id, 1)
device.syn()
time.sleep(2.5) # Wait for completion
device.erase_effect(effect_id)
finally:
device.close()from evdev import InputDevice, ecodes
from evdev.ff import Effect, Condition, Replay
import ctypes
device = InputDevice('/dev/input/event0')
# Create spring effect
effect = Effect()
effect.type = ecodes.FF_SPRING
effect.id = -1
effect.direction = 0x0000
# Long duration (until manually stopped)
effect.ff_replay.length = 0 # Infinite
effect.ff_replay.delay = 0
# No trigger
effect.ff_trigger.button = 0
effect.ff_trigger.interval = 0
# Spring parameters
effect.u.condition[0].right_saturation = 0x7FFF # Maximum right force
effect.u.condition[0].left_saturation = 0x7FFF # Maximum left force
effect.u.condition[0].right_coeff = 0x2000 # Spring coefficient
effect.u.condition[0].left_coeff = 0x2000 # Spring coefficient
effect.u.condition[0].deadband = 0x0000 # No deadband
effect.u.condition[0].center = 0x0000 # Center position
try:
effect_id = device.upload_effect(effect)
print("Spring effect active - move joystick to feel resistance")
# Start spring effect
device.write(ecodes.EV_FF, effect_id, 1)
device.syn()
time.sleep(5) # Let user test spring effect
# Stop effect
device.write(ecodes.EV_FF, effect_id, 0)
device.syn()
device.erase_effect(effect_id)
finally:
device.close()from evdev import InputDevice, ecodes
from evdev.ff import Effect, Constant, Replay, Envelope
import ctypes
import time
device = InputDevice('/dev/input/event0')
# Create constant force effect
effect = Effect()
effect.type = ecodes.FF_CONSTANT
effect.id = -1
effect.direction = 0x0000 # North direction
# Timing
effect.ff_replay.length = 1500 # 1.5 seconds
effect.ff_replay.delay = 0
# Constant force parameters
effect.u.constant.level = 0x4000 # 50% force level
# Envelope for smooth transitions
effect.u.constant.ff_envelope.attack_length = 300
effect.u.constant.ff_envelope.attack_level = 0x0000
effect.u.constant.ff_envelope.fade_length = 300
effect.u.constant.ff_envelope.fade_level = 0x0000
try:
effect_id = device.upload_effect(effect)
device.write(ecodes.EV_FF, effect_id, 1)
device.syn()
time.sleep(2)
device.erase_effect(effect_id)
finally:
device.close()from evdev import UInput, ecodes
from evdev.ff import Effect, Rumble, Replay, UInputUpload
import ctypes
# Create virtual device with force feedback capability
ui = UInput({
ecodes.EV_FF: [ecodes.FF_RUMBLE, ecodes.FF_PERIODIC, ecodes.FF_CONSTANT]
}, name='virtual-ff-device', max_effects=16)
try:
print(f"Virtual FF device created: {ui.device.path}")
# Handle force feedback upload requests
# This would typically be done in an event loop
# monitoring the UInput device for upload requests
# Example: Manual effect creation for testing
effect = Effect()
effect.type = ecodes.FF_RUMBLE
effect.id = 0
effect.ff_replay.length = 500
effect.u.rumble.strong_magnitude = 0x8000
# In a real application, you would:
# 1. Monitor ui.device for EV_UINPUT events
# 2. Handle UI_FF_UPLOAD and UI_FF_ERASE requests
# 3. Use ui.begin_upload/end_upload for effect management
print("Force feedback device ready")
time.sleep(5) # Keep device alive for testing
finally:
ui.close()from evdev import InputDevice, ecodes
from evdev.ff import Effect, Rumble, Replay
import ctypes
import time
device = InputDevice('/dev/input/event0')
# Create multiple effects
effects = []
# Light rumble
effect1 = Effect()
effect1.type = ecodes.FF_RUMBLE
effect1.id = -1
effect1.ff_replay.length = 500
effect1.u.rumble.strong_magnitude = 0x4000
effect1.u.rumble.weak_magnitude = 0x2000
# Strong rumble
effect2 = Effect()
effect2.type = ecodes.FF_RUMBLE
effect2.id = -1
effect2.ff_replay.length = 200
effect2.u.rumble.strong_magnitude = 0xFFFF
effect2.u.rumble.weak_magnitude = 0x8000
try:
# Upload all effects
effect_ids = []
for i, effect in enumerate([effect1, effect2]):
effect_id = device.upload_effect(effect)
effect_ids.append(effect_id)
print(f"Effect {i+1} uploaded with ID: {effect_id}")
# Play effects in sequence
for i, effect_id in enumerate(effect_ids):
print(f"Playing effect {i+1}")
device.write(ecodes.EV_FF, effect_id, 1)
device.syn()
time.sleep(0.8) # Wait between effects
print("Cleaning up effects...")
# Remove all effects
for effect_id in effect_ids:
device.erase_effect(effect_id)
finally:
device.close()from evdev import InputDevice, ecodes, resolve_ecodes
device = InputDevice('/dev/input/event0')
caps = device.capabilities()
if ecodes.EV_FF in caps:
ff_capabilities = caps[ecodes.EV_FF]
print(f"Force feedback capabilities: {len(ff_capabilities)} effects")
# Resolve FF capability names
resolved = resolve_ecodes(ecodes.FF, ff_capabilities)
for effect_info in resolved:
if isinstance(effect_info, tuple):
name, code = effect_info
print(f" {name} ({code})")
else:
print(f" {effect_info}")
# Check specific capabilities
if ecodes.FF_RUMBLE in ff_capabilities:
print("Device supports rumble effects")
if ecodes.FF_PERIODIC in ff_capabilities:
print("Device supports periodic effects")
if ecodes.FF_CONSTANT in ff_capabilities:
print("Device supports constant force effects")
if ecodes.FF_SPRING in ff_capabilities:
print("Device supports spring effects")
print(f"Maximum effects: {device.ff_effects_count}")
else:
print("Device does not support force feedback")
device.close()Install with Tessl CLI
npx tessl i tessl/pypi-evdev