-
Notifications
You must be signed in to change notification settings - Fork 119
Description
Bug
InputDevice.upload_effect(effect) returns the kernel-assigned effect ID but does not
write it back into the effect object's .id field. Since the initial Effect() is
constructed with id=-1 (meaning "create new"), every subsequent call to
upload_effect(effect) creates a new effect instead of updating the existing one.
This silently fills all available FF effect slots (typically 16 on consumer wheels)
within fractions of a second. After that, upload_effect raises OSError: [Errno 28] No
space left on device, and force feedback dies.
Root cause
def upload_effect(self, effect):
data = memoryview(effect).tobytes() # <-- creates a COPY
ff_id = _input.upload_effect(self.fd, data) # kernel writes ID into data, not
effect
return ff_id
memoryview(effect).tobytes() creates a byte copy. The kernel's EVIOCSFF ioctl writes
the assigned ID into that copy's buffer. The original effect ctypes struct is never
updated, so effect.id stays -1.
Reproduction
import evdev
from evdev import ecodes, ff
dev = evdev.InputDevice('/dev/input/event17') # any FF-capable device
envelope = ff.Envelope(0, 0, 0, 0)
constant = ff.Constant(16384, envelope)
effect = ff.Effect(
ecodes.FF_CONSTANT, -1, 16384,
ff.Trigger(0, 0),
ff.Replay(0, 0),
ff.EffectType(ff_constant_effect=constant)
)
. # First upload — works, returns e.g. 0
eid = dev.upload_effect(effect)
print(f"Returned ID: {eid}, effect.id: {effect.id}")
. # Output: Returned ID: 0, effect.id: -1 <-- BUG: should be 0
. # Second upload — creates ANOTHER effect instead of updating #0
eid2 = dev.upload_effect(effect)
print(f"Returned ID: {eid2}")
. # Output: Returned ID: 1 <-- new effect, not an update
. # After 16 uploads: OSError: [Errno 28] No space left on device
Expected behavior
After upload_effect(), effect.id should be set to the kernel-assigned ID, so that
subsequent calls update the existing effect in-place (the standard Linux FF pattern).
Workaround
Manually set the ID after the first upload:
eid = dev.upload_effect(effect)
effect.id = eid # <-- required workaround
. # Now re-uploads update in-place
constant.level = 32767
effect.u.ff_constant_effect = constant
dev.upload_effect(effect) # updates effect #0, doesn't create new
Impact
This affects anyone using upload_effect in a loop for continuous force feedback — the
standard pattern for driving simulators, haptic feedback, and any application using
FF_CONSTANT updated per-frame. Single-shot effects are unaffected.
The failure mode is particularly insidious: FF works for the first 16 updates (~0.3
seconds at 60 Hz), then silently dies or throws an opaque ENOSPC error that doesn't
suggest the real cause.
Suggested fix
Either:
A) Write the ID back into the effect object (preferred — matches user expectations):
def upload_effect(self, effect):
data = memoryview(effect).tobytes()
ff_id = _input.upload_effect(self.fd, data)
effect.id = ff_id # <-- write back
return ff_id
B) Document the behavior prominently in the docstring and tutorial, with the
workaround.
Environment
- python-evdev 1.9.3
- Linux 6.18.8 (also confirmed conceptually for any kernel version)
- Device: Logitech G29 (hid-logitech / new-lg4ff), but the bug is device-independent
- found by: Claude Code Opus 4.5 (topoMars project)