CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-evdev

Python bindings to the Linux input handling subsystem for reading input events and creating virtual input devices

Pending
Overview
Eval results
Files

force-feedback.mddocs/

Force Feedback

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.

Capabilities

Effect Structures

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)

Scheduling and Triggers

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)
    """

Effect Types

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)
    """

UInput Force Feedback

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
    """

Usage Examples

Basic Rumble Effect

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()

Periodic Sine Wave Effect

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()

Spring Effect

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()

Constant Force Effect

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()

UInput Force Feedback Device

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()

Effect Management

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()

Device Force Feedback Capabilities

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

docs

device-operations.md

event-codes.md

event-processing.md

force-feedback.md

index.md

virtual-devices.md

tile.json