Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
__pycache__
/extra
*~
/.dev-locale
/run.py
71 changes: 71 additions & 0 deletions data/udev/99-oversteer-wheel-power.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Allow regular users to enable/disable supported wheels from Oversteer.
#
# Oversteer's power switch writes to the USB device's "authorized" attribute
# (1 = visible to the system, 0 = hidden from every game/application). That
# attribute lives at the usb_device node and is owned by root, so this rule
# grants write access to it for the supported wheels only, avoiding any other
# USB device (mice, keyboards, etc.).
#
# The usb_device node and its "authorized" attribute exist as long as the wheel
# is plugged in, even while deauthorized, so this single rule covers both
# disabling and re-enabling.

ACTION=="remove", GOTO="oversteer_power_end"
SUBSYSTEM!="usb", GOTO="oversteer_power_end"
ENV{DEVTYPE}!="usb_device", GOTO="oversteer_power_end"

# Logitech wheels
ATTR{idVendor}=="046d", ATTR{idProduct}=="c294", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c29a", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c298", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c299", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c29b", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c24f", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c262", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c266", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c26e", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c268", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c272", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c295", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="ca03", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c29c", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c291", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c293", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="046d", ATTR{idProduct}=="c20e", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"

# Thrustmaster wheels
ATTR{idVendor}=="044f", ATTR{idProduct}=="b677", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b696", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b66e", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b66f", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b66d", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b65e", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b668", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b66a", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b67f", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b664", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b692", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b669", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="044f", ATTR{idProduct}=="b689", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"

# Other Thrustmaster (different vendor IDs)
ATTR{idVendor}=="24c6", ATTR{idProduct}=="5b00", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="06f8", ATTR{idProduct}=="0004", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"

# Fanatec wheels
ATTR{idVendor}=="0eb7", ATTR{idProduct}=="0020", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="0eb7", ATTR{idProduct}=="0e03", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="0eb7", ATTR{idProduct}=="0005", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="0eb7", ATTR{idProduct}=="0001", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="0eb7", ATTR{idProduct}=="0004", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="0eb7", ATTR{idProduct}=="0006", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="0eb7", ATTR{idProduct}=="0007", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"

# Cammus
ATTR{idVendor}=="3416", ATTR{idProduct}=="0301", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"

# Generic FFBoard / Flashfire
ATTR{idVendor}=="1209", ATTR{idProduct}=="ffb0", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"
ATTR{idVendor}=="2f24", ATTR{idProduct}=="010d", RUN+="/bin/sh -c 'chmod 666 %S%p/authorized'"

LABEL="oversteer_power_end"
30 changes: 30 additions & 0 deletions oversteer/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(self, device_manager, data):
self.product_id = None
self.usb_id = None
self.dev_path = None
self.usb_path = None
self.dev_name = None
self.name = None
self.ready = True
Expand Down Expand Up @@ -82,6 +83,35 @@ def check_file_permissions(self, filename):
def get_max_range(self):
return self.max_range

def authorized_file(self):
if self.usb_path is None:
return None
return os.path.join(self.usb_path, 'authorized')

def is_authorized(self):
path = self.authorized_file()
if path is None or not os.access(path, os.F_OK | os.R_OK):
return None
with open(path, "r") as file:
data = file.read()
return int(data.strip()) == 1

def can_set_authorized(self):
path = self.authorized_file()
if path is None:
return False
return os.access(path, os.W_OK)

def set_authorized(self, authorized):
path = self.authorized_file()
if path is None:
return False
value = "1" if authorized else "0"
logging.debug("Setting authorized: %s (%s)", value, path)
with open(path, "w") as file:
file.write(value)
return True

def list_modes(self):
path = self.checked_device_file("alternate_modes")
if not path:
Expand Down
18 changes: 18 additions & 0 deletions oversteer/device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,17 @@ def update_device_list(self, udevice):
logging.debug("%s: ID_VENDOR_ID: %s ID_MODEL_ID: %s", device_node,
udevice.get('ID_VENDOR_ID'), udevice.get('ID_MODEL_ID'))

usb_device = udevice.find_parent('usb', 'usb_device')
usb_path = usb_device.sys_path if usb_device is not None else None

device.set({
'id': id,
'vendor_id': udevice.get('ID_VENDOR_ID'),
'product_id': udevice.get('ID_MODEL_ID'),
'usb_id': usb_id,
'dev_name': device_node,
'dev_path': os.path.realpath(os.path.join(udevice.sys_path, 'device', 'device')),
'usb_path': usb_path,
'name': bytes(udevice.get('ID_VENDOR_ENC') + ' ' + udevice.get('ID_MODEL_ENC'),
'utf-8').decode('unicode_escape'),
'max_range': self.supported_wheels[usb_id],
Expand All @@ -144,6 +148,20 @@ def get_device(self, did):
return self.devices[did]
return next((item for item in self.devices.values() if item.dev_name == did), None)

def authorize_path(self, usb_path, authorized):
# Re-enable a wheel that is no longer in the device list (it left the
# input subsystem when it was deauthorized). The usb_device node and its
# 'authorized' attribute still exist while the wheel stays plugged in.
if usb_path is None:
return False
path = os.path.join(usb_path, 'authorized')
if not os.access(path, os.F_OK | os.W_OK):
return False
with open(path, "w") as file:
file.write("1" if authorized else "0")
self.changed = True
return True

def is_changed(self):
changed = self.changed
self.changed = False
Expand Down
6 changes: 6 additions & 0 deletions oversteer/gtk_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ def on_update_clicked(self, button):
self.controller.populate_window()
self.ui.update()

def on_wheel_power_state_set(self, widget, state):
if self.ui._suppress_power_signal:
return False
self.controller.set_wheel_power(state)
return False

def on_change_emulation_mode_clicked(self, widget):
mode = self.ui.emulation_mode_combobox.get_active_id()
self.model.set_mode(mode)
Expand Down
17 changes: 17 additions & 0 deletions oversteer/gtk_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def __init__(self, controller, argv):
self.ffbmeter_timer = False
self.current_test_canvas = None
self.current_test_toolbar = None
self._suppress_power_signal = False

Gdk.init(argv)
style_provider = Gtk.CssProvider()
Expand Down Expand Up @@ -168,6 +169,20 @@ def set_check_permissions(self, state):
def set_device_id(self, device_id):
self.device_combobox.set_active_id(device_id)

def set_wheel_power(self, state, sensitive = True):
# Update the switch without triggering a write back to the device.
self._suppress_power_signal = True
self.wheel_power.set_sensitive(sensitive and state is not None)
self.wheel_power.set_active(bool(state))
self._suppress_power_signal = False

def set_wheel_power_status(self, text):
if text:
self.wheel_power_status.set_label(text)
self.wheel_power_status.show()
else:
self.wheel_power_status.hide()

def set_devices(self, devices):
model = self.device_combobox.get_model()
if model is None:
Expand Down Expand Up @@ -578,6 +593,8 @@ def _set_builder_objects(self):
self.check_permissions = self.builder.get_object('check_permissions')

self.device_combobox = self.builder.get_object('device')
self.wheel_power = self.builder.get_object('wheel_power')
self.wheel_power_status = self.builder.get_object('wheel_power_status')
self.profile_combobox = self.builder.get_object('profile')
self.new_profile_name_entry = self.builder.get_object('new_profile_name')
self.save_profile_button = self.builder.get_object('save_profile')
Expand Down
57 changes: 57 additions & 0 deletions oversteer/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def __init__(self, application, model, argv):
self.model = model
self.device_manager = self.app.device_manager
self.device = None
self.disabled_wheel = None
self.grab_input = False
self.test = None
self.linear_chart = None
Expand Down Expand Up @@ -155,6 +156,7 @@ def populate_devices(self):
if device.is_ready():
device_list.append((device.get_id(), device.name))
self.ui.set_devices(device_list)
self.update_power_controls()

def populate_profiles(self):
profiles = []
Expand All @@ -166,6 +168,52 @@ def populate_profiles(self):
def populate_window(self):
self.populate_devices()
self.populate_profiles()
self.update_power_controls()

def update_power_controls(self):
# A disabled wheel leaves the device list, so the switch keeps
# representing it (off, with a status message) until it's re-enabled.
if self.disabled_wheel is not None:
self.ui.set_wheel_power(False, True)
self.ui.set_wheel_power_status(_("Re-enable {}").format(self.disabled_wheel['name']))
elif self.device is not None and self.device.is_authorized() is not None:
self.ui.set_wheel_power(self.device.is_authorized(), self.device.can_set_authorized())
self.ui.set_wheel_power_status(None)
else:
self.ui.set_wheel_power(False, False)
self.ui.set_wheel_power_status(None)

def set_wheel_power(self, state):
try:
if state:
if self.disabled_wheel is not None:
# Re-enable the remembered wheel; it's no longer in the list.
if not self.device_manager.authorize_path(self.disabled_wheel['usb_path'], True):
raise PermissionError
self.disabled_wheel = None
self.save_preferences()
elif self.device is not None:
self.device.set_authorized(True)
else:
if self.device is None:
return
self.device.set_authorized(False)
# Remember the wheel so it can be re-enabled with the switch
# after it disappears from the device list.
self.disabled_wheel = {
'usb_path': self.device.usb_path,
'name': self.device.name,
}
self.save_preferences()
except (KeyboardInterrupt, SystemExit):
raise
except OSError:
if self.app.udev_path:
self.install_udev_files()
else:
self.ui.info_dialog(_("You don't have the required permissions to " +
"enable or disable the wheel."))
self.update_power_controls()

def change_device(self, device_id):
self.device = self.device_manager.get_device(device_id)
Expand All @@ -188,6 +236,8 @@ def change_device(self, device_id):
self.model = Model(self.device, self.ui)
self.models[self.device.get_id()] = self.model

self.update_power_controls()

self.ui.set_max_range(self.device.get_max_range())
self.ui.set_modes(self.model.get_mode_list())

Expand Down Expand Up @@ -265,6 +315,11 @@ def load_preferences(self):
Locale.setlocale(Locale.LC_ALL, (self.locale, 'UTF-8'))
if 'check_permissions' in config['DEFAULT']:
self.check_permissions = config['DEFAULT']['check_permissions'] == '1'
if config['DEFAULT'].get('disabled_wheel_path', '') != '':
self.disabled_wheel = {
'usb_path': config['DEFAULT']['disabled_wheel_path'],
'name': config['DEFAULT'].get('disabled_wheel_name', _('wheel')),
}
if 'button_config' in config['DEFAULT'] and config['DEFAULT']['button_config'] != '':
if 'button_toggle' not in config['DEFAULT']:
self.button_config = list(map(int, config['DEFAULT']['button_config'].split(',')))
Expand Down Expand Up @@ -298,6 +353,8 @@ def save_preferences(self):
'check_permissions': '1' if self.check_permissions else '0',
'button_toggle': ','.join(map(str, self.button_config[0])),
'button_config': ','.join(map(str, self.button_config[1:])),
'disabled_wheel_path': self.disabled_wheel['usb_path'] if self.disabled_wheel else '',
'disabled_wheel_name': self.disabled_wheel['name'] if self.disabled_wheel else '',
}
config_file = os.path.join(self.config_path, 'config.ini')
with open(config_file, 'w') as file:
Expand Down
54 changes: 54 additions & 0 deletions oversteer/main.ui
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,60 @@
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">8</property>
<property name="tooltip-text" translatable="yes">Enable or disable the wheel for the whole system. When disabled, no game or application can see it.</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Power</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="wheel_power">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="valign">center</property>
<property name="active">True</property>
<signal name="state-set" handler="on_wheel_power_state_set" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="wheel_power_status">
<property name="can-focus">False</property>
<property name="no-show-all">True</property>
<property name="valign">center</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
Expand Down
19 changes: 19 additions & 0 deletions po/ca.po
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,25 @@ msgstr "Configurador de volants"
msgid "game;racing;cars;wheels"
msgstr "jocs;carreres;coches;volants"

#: oversteer/main.ui:369
msgid "Power"
msgstr "Encesa"

#: oversteer/main.ui:364
msgid ""
"Enable or disable the wheel for the whole system. When disabled, no game or "
"application can see it."
msgstr "Activa o desactiva el volant per a tot el sistema. Quan està desactivat, cap joc o aplicació no el pot veure."

#: oversteer/gui.py:178
#, python-brace-format
msgid "Re-enable {}"
msgstr "Reactiva {}"

#: oversteer/gui.py:214
msgid "You don't have the required permissions to enable or disable the wheel."
msgstr "No tens els permissos requerits per a activar o desactivar el volant."

#~ msgid "io.github.berarma.Oversteer"
#~ msgstr "io.github.berarma.Oversteer"

Expand Down
Loading