937 lines
35 KiB
Python
937 lines
35 KiB
Python
# Fenrir TTY screen reader
|
|
# By Chrys, Storm Dragon, and contributors.
|
|
|
|
import multiprocessing
|
|
import threading
|
|
import time
|
|
from ctypes import c_bool
|
|
from multiprocessing.sharedctypes import Value
|
|
from select import select
|
|
|
|
from fenrirscreenreader.core import debug
|
|
from fenrirscreenreader.core import inputData
|
|
from fenrirscreenreader.core.eventData import FenrirEventType
|
|
from fenrirscreenreader.core.inputDriver import InputDriver as inputDriver
|
|
|
|
_evdevAvailable = False
|
|
_udevAvailable = False
|
|
_evdevAvailableError = ""
|
|
_udevAvailableError = ""
|
|
try:
|
|
import evdev
|
|
from evdev import InputDevice
|
|
from evdev import UInput
|
|
from evdev import ecodes as e
|
|
|
|
_evdevAvailable = True
|
|
|
|
except Exception as e:
|
|
_evdevAvailableError = str(e)
|
|
|
|
try:
|
|
import pyudev
|
|
|
|
_udevAvailable = True
|
|
except Exception as e:
|
|
_udevAvailableError = str(e)
|
|
|
|
|
|
class driver(inputDriver):
|
|
"""Linux evdev input driver for Fenrir screen reader.
|
|
|
|
This driver provides access to Linux input devices through the evdev interface,
|
|
allowing Fenrir to capture keyboard input, manage device grabbing for exclusive
|
|
access, and inject synthetic input events.
|
|
|
|
Features:
|
|
- Automatic device detection and hotplug support via udev
|
|
- Device grabbing to prevent input from reaching other applications
|
|
- Key event mapping and filtering
|
|
- UInput support for synthetic key injection
|
|
- Multi-device support with thread-safe access
|
|
|
|
Attributes:
|
|
iDevices (dict): Map of file descriptor to InputDevice objects
|
|
iDevicesFD (list): Shared list of file descriptors for multiprocessing
|
|
uDevices (dict): Map of file descriptor to UInput devices
|
|
gDevices (dict): Map of file descriptor to grab status
|
|
iDeviceNo (int): Total number of input devices
|
|
UInputinject (UInput): Device for injecting synthetic events
|
|
_deviceLock (Lock): Thread lock for device access
|
|
"""
|
|
def __init__(self):
|
|
inputDriver.__init__(self)
|
|
self._manager = multiprocessing.Manager()
|
|
self.iDevices = {}
|
|
self.iDevicesFD = self._manager.list()
|
|
self.uDevices = {}
|
|
self.gDevices = {}
|
|
self.iDeviceNo = 0
|
|
self.watch_dog = Value(c_bool, True)
|
|
self.UInputinject = UInput()
|
|
self._deviceLock = threading.Lock()
|
|
|
|
def initialize(self, environment):
|
|
"""Initialize the evdev input driver.
|
|
|
|
Sets up device monitoring, starts watchdog threads for device hotplug
|
|
detection and input monitoring, and configures the input subsystem.
|
|
|
|
Args:
|
|
environment: Fenrir environment dictionary with runtime managers
|
|
|
|
Note:
|
|
Requires evdev and optionally pyudev libraries. Falls back gracefully
|
|
if libraries are not available.
|
|
"""
|
|
self.env = environment
|
|
self.env["runtime"]["InputManager"].set_shortcut_type("KEY")
|
|
global _evdevAvailable
|
|
global _udevAvailable
|
|
global _evdevAvailableError
|
|
global _udevAvailableError
|
|
if not _udevAvailable:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver:" + _udevAvailableError, debug.DebugLevel.ERROR
|
|
)
|
|
if not _evdevAvailable:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver:" + _evdevAvailableError, debug.DebugLevel.ERROR
|
|
)
|
|
return
|
|
|
|
if _udevAvailable:
|
|
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
|
|
self.plug_input_device_watchdog_udev
|
|
)
|
|
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
|
|
self.input_watchdog
|
|
)
|
|
self._initialized = True
|
|
|
|
def plug_input_device_watchdog_udev(self, active, event_queue):
|
|
"""Monitor for input device hotplug events via udev.
|
|
|
|
Runs in a separate thread to detect when input devices are
|
|
plugged/unplugged and generates appropriate events for device
|
|
management.
|
|
|
|
Args:
|
|
active: Shared boolean controlling the watchdog loop
|
|
event_queue: Queue for sending device events to main process
|
|
|
|
Events Generated:
|
|
FenrirEventType.plug_input_device: When new devices are detected
|
|
|
|
Note:
|
|
Filters out virtual devices and devices from assistive technologies
|
|
like BRLTTY to avoid conflicts.
|
|
"""
|
|
context = pyudev.Context()
|
|
monitor = pyudev.Monitor.from_netlink(context)
|
|
monitor.filter_by(subsystem="input")
|
|
monitor.start()
|
|
ignore_plug = False
|
|
while active.value:
|
|
valid_devices = []
|
|
device = monitor.poll(1)
|
|
while device:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"plug_input_device_watchdog_udev:" + str(device),
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
try:
|
|
try:
|
|
# FIX: Check if attributes exist before accessing them
|
|
if (
|
|
hasattr(device, "name")
|
|
and device.name
|
|
and device.name.upper()
|
|
in ["", "SPEAKUP", "FENRIR-UINPUT"]
|
|
):
|
|
ignore_plug = True
|
|
if (
|
|
hasattr(device, "phys")
|
|
and device.phys
|
|
and device.phys.upper()
|
|
in ["", "SPEAKUP", "FENRIR-UINPUT"]
|
|
):
|
|
ignore_plug = True
|
|
if (
|
|
hasattr(device, "name")
|
|
and device.name
|
|
and "BRLTTY" in device.name.upper()
|
|
):
|
|
ignore_plug = True
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"plug_input_device_watchdog_udev CHECK NAME CRASH: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
|
|
if not ignore_plug:
|
|
virtual = (
|
|
"/sys/devices/virtual/input/" in device.sys_path
|
|
)
|
|
if device.device_node:
|
|
valid_devices.append(
|
|
{
|
|
"device": device.device_node,
|
|
"virtual": virtual,
|
|
}
|
|
)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"plug_input_device_watchdog_udev APPEND CRASH: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
poll_timeout = 1
|
|
device = monitor.poll(poll_timeout)
|
|
except Exception:
|
|
device = None
|
|
ignore_plug = False
|
|
if valid_devices:
|
|
event_queue.put(
|
|
{
|
|
"Type": FenrirEventType.plug_input_device,
|
|
"data": valid_devices,
|
|
}
|
|
)
|
|
return time.time()
|
|
|
|
def input_watchdog(self, active, event_queue):
|
|
"""Main input monitoring loop.
|
|
|
|
Monitors all registered input devices for key events using select().
|
|
Processes key events, maps them to Fenrir's internal format, and
|
|
forwards them to the main application.
|
|
|
|
Args:
|
|
active: Shared boolean controlling the watchdog loop
|
|
event_queue: Queue for sending input events to main process
|
|
|
|
Events Generated:
|
|
FenrirEventType.keyboard_input: For key press/release events
|
|
|
|
Note:
|
|
Uses thread-safe device access and handles device disconnection
|
|
gracefully. Non-keyboard events are forwarded to UInput devices.
|
|
"""
|
|
try:
|
|
while active.value:
|
|
# Get a snapshot of devices for select() to avoid lock
|
|
# contention
|
|
with self._deviceLock:
|
|
devices_snapshot = self.iDevices.copy()
|
|
|
|
if not devices_snapshot:
|
|
time.sleep(0.1)
|
|
continue
|
|
|
|
r, w, x = select(devices_snapshot, [], [], 0.8)
|
|
event = None
|
|
found_key_in_sequence = False
|
|
foreward = False
|
|
event_fired = False
|
|
for fd in r:
|
|
# Check if device still exists before accessing
|
|
with self._deviceLock:
|
|
if fd not in self.iDevices:
|
|
continue
|
|
device = self.iDevices[fd]
|
|
udevice = self.uDevices.get(fd)
|
|
|
|
try:
|
|
event = device.read_one()
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver handle_input_event: Error reading event: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
self.remove_device(fd)
|
|
continue
|
|
while event:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"input_watchdog: EVENT:" + str(event),
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
self.env["input"]["eventBuffer"].append(
|
|
[device, udevice, event]
|
|
)
|
|
if event.type == evdev.events.EV_KEY:
|
|
if not found_key_in_sequence:
|
|
found_key_in_sequence = True
|
|
if event.code != 0:
|
|
curr_map_event = self.map_event(event)
|
|
if not curr_map_event:
|
|
event = device.read_one()
|
|
continue
|
|
if not isinstance(
|
|
curr_map_event["EventName"], str
|
|
):
|
|
event = device.read_one()
|
|
continue
|
|
if curr_map_event["EventState"] in [0, 1, 2]:
|
|
event_queue.put(
|
|
{
|
|
"Type": FenrirEventType.keyboard_input,
|
|
"data": curr_map_event.copy(),
|
|
}
|
|
)
|
|
event_fired = True
|
|
else:
|
|
if event.type in [2, 3]:
|
|
foreward = True
|
|
|
|
event = device.read_one()
|
|
if not found_key_in_sequence:
|
|
if foreward and not event_fired:
|
|
self.write_event_buffer()
|
|
self.clear_event_buffer()
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"INPUT WATCHDOG CRASH: " + str(e), debug.DebugLevel.ERROR
|
|
)
|
|
|
|
def write_event_buffer(self):
|
|
if not self._initialized:
|
|
return
|
|
for iDevice, uDevice, event in self.env["input"]["eventBuffer"]:
|
|
try:
|
|
if uDevice:
|
|
if self.gDevices[iDevice.fd]:
|
|
self.write_u_input(uDevice, event)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver write_event_buffer: Error writing event: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
|
|
def write_u_input(self, uDevice, event):
|
|
if not self._initialized:
|
|
return
|
|
uDevice.write_event(event)
|
|
time.sleep(0.0000002)
|
|
uDevice.syn()
|
|
|
|
def update_input_devices(self, new_devices=None, init=False):
|
|
"""Update the list of monitored input devices.
|
|
|
|
Scans for available input devices and adds suitable ones based on
|
|
the configured device mode. Supports filtering by device type and
|
|
name matching.
|
|
|
|
Args:
|
|
new_devices (list, optional): Specific devices to add
|
|
init (bool): Whether this is initial device setup
|
|
|
|
Device Modes:
|
|
- 'ALL': Monitor all keyboard devices
|
|
- 'NOMICE': Monitor keyboards but exclude pointing devices
|
|
- Device names: Comma-separated list of specific device names
|
|
|
|
Note:
|
|
Automatically filters out virtual devices, assistive technology
|
|
devices, and devices with insufficient key counts.
|
|
"""
|
|
if init:
|
|
self.remove_all_devices()
|
|
|
|
device_file_list = None
|
|
|
|
if new_devices and not init:
|
|
if not isinstance(new_devices, list):
|
|
new_devices = [new_devices]
|
|
device_file_list = new_devices
|
|
else:
|
|
device_file_list = evdev.list_devices()
|
|
if len(device_file_list) == self.iDeviceNo:
|
|
return
|
|
if not device_file_list:
|
|
return
|
|
|
|
mode = (
|
|
self.env["runtime"]["SettingsManager"]
|
|
.get_setting("keyboard", "device")
|
|
.upper()
|
|
)
|
|
|
|
i_devices_files = []
|
|
for device in self.iDevices:
|
|
i_devices_files.append(self.iDevices[device].path)
|
|
|
|
event_type = evdev.events
|
|
for deviceFile in device_file_list:
|
|
try:
|
|
if not deviceFile:
|
|
continue
|
|
if deviceFile == "":
|
|
continue
|
|
if deviceFile in i_devices_files:
|
|
continue
|
|
try:
|
|
with open(deviceFile) as f:
|
|
pass
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver update_input_devices: Error opening device file: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
continue
|
|
# 3 pos absolute
|
|
# 2 pos relative
|
|
# 1 Keys
|
|
try:
|
|
curr_device = evdev.InputDevice(deviceFile)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver createDeviceType: Error creating device: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
continue
|
|
try:
|
|
# FIX: Check if attributes exist before accessing them
|
|
if (
|
|
hasattr(curr_device, "name")
|
|
and curr_device.name
|
|
and curr_device.name.upper()
|
|
in ["", "SPEAKUP", "FENRIR-UINPUT"]
|
|
):
|
|
continue
|
|
if (
|
|
hasattr(curr_device, "phys")
|
|
and curr_device.phys
|
|
and curr_device.phys.upper()
|
|
in ["", "SPEAKUP", "FENRIR-UINPUT"]
|
|
):
|
|
continue
|
|
if (
|
|
hasattr(curr_device, "name")
|
|
and curr_device.name
|
|
and "BRLTTY" in curr_device.name.upper()
|
|
):
|
|
continue
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver: Error checking device capabilities: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
cap = curr_device.capabilities()
|
|
if mode in ["ALL", "NOMICE"]:
|
|
if event_type.EV_KEY in cap:
|
|
if (
|
|
116 in cap[event_type.EV_KEY]
|
|
and len(cap[event_type.EV_KEY]) < 10
|
|
):
|
|
self.env["runtime"][
|
|
"DebugManager"
|
|
].write_debug_out(
|
|
"Device Skipped (has 116):" + curr_device.name,
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
continue
|
|
if len(cap[event_type.EV_KEY]) < 60:
|
|
self.env["runtime"][
|
|
"DebugManager"
|
|
].write_debug_out(
|
|
"Device Skipped (< 60 keys):"
|
|
+ curr_device.name,
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
continue
|
|
if mode == "ALL":
|
|
self.add_device(curr_device)
|
|
self.env["runtime"][
|
|
"DebugManager"
|
|
].write_debug_out(
|
|
"Device added (ALL):"
|
|
+ self.iDevices[curr_device.fd].name,
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
elif mode == "NOMICE":
|
|
if not (
|
|
(event_type.EV_REL in cap)
|
|
or (event_type.EV_ABS in cap)
|
|
):
|
|
self.add_device(curr_device)
|
|
self.env["runtime"][
|
|
"DebugManager"
|
|
].write_debug_out(
|
|
"Device added (NOMICE):"
|
|
+ self.iDevices[curr_device.fd].name,
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
else:
|
|
self.env["runtime"][
|
|
"DebugManager"
|
|
].write_debug_out(
|
|
"Device Skipped (NOMICE):"
|
|
+ curr_device.name,
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
else:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"Device Skipped (no EV_KEY):" + curr_device.name,
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
elif curr_device.name.upper() in mode.split(","):
|
|
self.add_device(curr_device)
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"Device added (Name):"
|
|
+ self.iDevices[curr_device.fd].name,
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
except Exception as e:
|
|
try:
|
|
device_name = (
|
|
curr_device.name
|
|
if hasattr(curr_device, "name")
|
|
else "unknown"
|
|
)
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"Device Skipped (Exception): "
|
|
+ deviceFile
|
|
+ " "
|
|
+ device_name
|
|
+ " "
|
|
+ str(e),
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
except Exception as ex:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"Device Skipped (Exception): "
|
|
+ deviceFile
|
|
+ " "
|
|
+ str(ex),
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
self.iDeviceNo = len(evdev.list_devices())
|
|
self.update_m_pi_devices_fd()
|
|
|
|
def update_m_pi_devices_fd(self):
|
|
try:
|
|
for fd in self.iDevices:
|
|
if fd not in self.iDevicesFD:
|
|
self.iDevicesFD.append(fd)
|
|
for fd in self.iDevicesFD:
|
|
if fd not in self.iDevices:
|
|
self.iDevicesFD.remove(fd)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver update_m_pi_devices_fd: Error updating device file descriptors: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
|
|
def map_event(self, event):
|
|
if not self._initialized:
|
|
return None
|
|
if not event:
|
|
return None
|
|
m_event = inputData.input_event
|
|
try:
|
|
# mute is a list = ['KEY_MIN_INTERESTING', 'KEY_MUTE']
|
|
m_event["EventName"] = evdev.ecodes.keys[event.code]
|
|
if isinstance(m_event["EventName"], list):
|
|
if len(m_event["EventName"]) > 0:
|
|
m_event["EventName"] = m_event["EventName"][0]
|
|
if isinstance(m_event["EventName"], list):
|
|
if len(m_event["EventName"]) > 0:
|
|
m_event["EventName"] = m_event["EventName"][0]
|
|
m_event["EventValue"] = event.code
|
|
m_event["EventSec"] = event.sec
|
|
m_event["EventUsec"] = event.usec
|
|
m_event["EventState"] = event.value
|
|
m_event["EventType"] = event.type
|
|
return m_event
|
|
except Exception as e:
|
|
return None
|
|
|
|
def get_led_state(self, led=0):
|
|
if not self.has_i_devices():
|
|
return False
|
|
# 0 = Numlock
|
|
# 1 = Capslock
|
|
# 2 = Rollen
|
|
for fd, dev in self.iDevices.items():
|
|
if led in dev.leds():
|
|
return True
|
|
return False
|
|
|
|
def toggle_led_state(self, led=0):
|
|
if not self.has_i_devices():
|
|
return False
|
|
led_state = self.get_led_state(led)
|
|
for i in self.iDevices:
|
|
if self.gDevices[i]:
|
|
# 17 LEDs
|
|
if 17 in self.iDevices[i].capabilities():
|
|
if led_state == 1:
|
|
self.iDevices[i].set_led(led, 0)
|
|
else:
|
|
self.iDevices[i].set_led(led, 1)
|
|
|
|
def grab_all_devices(self):
|
|
if not self._initialized:
|
|
return True
|
|
ok = True
|
|
for fd in self.iDevices:
|
|
if not self.gDevices[fd]:
|
|
ok = ok and self.grab_device(fd)
|
|
return ok
|
|
|
|
def ungrab_all_devices(self):
|
|
if not self._initialized:
|
|
return True
|
|
ok = True
|
|
for fd in self.iDevices:
|
|
if self.gDevices[fd]:
|
|
ok = ok and self.ungrab_device(fd)
|
|
return ok
|
|
|
|
def create_u_input_dev(self, fd):
|
|
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
|
"keyboard", "grabDevices"
|
|
):
|
|
self.uDevices[fd] = None
|
|
return
|
|
try:
|
|
test = self.uDevices[fd]
|
|
return
|
|
except KeyError:
|
|
self.uDevices[fd] = None
|
|
if self.uDevices[fd] is not None:
|
|
return
|
|
try:
|
|
self.uDevices[fd] = UInput.from_device(
|
|
self.iDevices[fd], name="fenrir-uinput", phys="fenrir-uinput"
|
|
)
|
|
except Exception as e:
|
|
try:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: compat fallback: " + str(e),
|
|
debug.DebugLevel.WARNING,
|
|
)
|
|
dev = self.iDevices[fd]
|
|
cap = dev.capabilities()
|
|
del cap[0]
|
|
self.uDevices[fd] = UInput(
|
|
cap, name="fenrir-uinput", phys="fenrir-uinput"
|
|
)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: init Uinput not possible: " + str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
return
|
|
|
|
def add_device(self, newDevice):
|
|
"""Add a new input device to the monitoring list.
|
|
|
|
Creates the necessary data structures for device monitoring,
|
|
sets up UInput forwarding if device grabbing is enabled, and
|
|
initializes device state.
|
|
|
|
Args:
|
|
newDevice: evdev.InputDevice object to add
|
|
|
|
Note:
|
|
Thread-safe operation. Automatically cleans up on failure.
|
|
"""
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: device added: "
|
|
+ str(newDevice.fd)
|
|
+ " "
|
|
+ str(newDevice),
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
with self._deviceLock:
|
|
try:
|
|
self.iDevices[newDevice.fd] = newDevice
|
|
self.create_u_input_dev(newDevice.fd)
|
|
self.gDevices[newDevice.fd] = False
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: error adding device: " + str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
# if it doesnt work clean up
|
|
try:
|
|
del self.iDevices[newDevice.fd]
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver remove_device: Error removing iDevice: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
del self.uDevices[newDevice.fd]
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver remove_device: Error removing uDevice: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
del self.gDevices[newDevice.fd]
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver add_device: Error cleaning up gDevice: "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
|
|
def grab_device(self, fd):
|
|
"""Grab exclusive access to an input device.
|
|
|
|
Takes exclusive control of the device, preventing other applications
|
|
from receiving its input. Also resets modifier key states to prevent
|
|
stuck keys.
|
|
|
|
Args:
|
|
fd (int): File descriptor of device to grab
|
|
|
|
Returns:
|
|
bool: True if grab successful, False otherwise
|
|
|
|
Note:
|
|
Only effective if grabDevices setting is enabled.
|
|
"""
|
|
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
|
"keyboard", "grabDevices"
|
|
):
|
|
return True
|
|
|
|
# FIX: Handle exception variable scope correctly
|
|
grab_error = None
|
|
try:
|
|
self.iDevices[fd].grab()
|
|
self.gDevices[fd] = True
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: grab device ("
|
|
+ str(self.iDevices[fd].name)
|
|
+ ")",
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
# Reset modifier keys on successful grab
|
|
if self.uDevices[fd]:
|
|
modifier_keys = [
|
|
e.KEY_LEFTCTRL,
|
|
e.KEY_RIGHTCTRL,
|
|
e.KEY_LEFTALT,
|
|
e.KEY_RIGHTALT,
|
|
e.KEY_LEFTSHIFT,
|
|
e.KEY_RIGHTSHIFT,
|
|
]
|
|
for key in modifier_keys:
|
|
try:
|
|
self.uDevices[fd].write(e.EV_KEY, key, 0) # 0 = key up
|
|
self.uDevices[fd].syn()
|
|
except Exception as mod_error:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"Failed to reset modifier key: " + str(mod_error),
|
|
debug.DebugLevel.WARNING,
|
|
)
|
|
except IOError:
|
|
if not self.gDevices[fd]:
|
|
return False
|
|
except Exception as ex:
|
|
grab_error = ex
|
|
|
|
if grab_error:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: grabing not possible: " + str(grab_error),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
def ungrab_device(self, fd):
|
|
"""Release exclusive access to an input device.
|
|
|
|
Returns control of the device to the system, allowing other
|
|
applications to receive its input.
|
|
|
|
Args:
|
|
fd (int): File descriptor of device to ungrab
|
|
|
|
Returns:
|
|
bool: True if ungrab successful, False otherwise
|
|
"""
|
|
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
|
"keyboard", "grabDevices"
|
|
):
|
|
return True
|
|
|
|
# FIX: Handle exception variable scope correctly
|
|
ungrab_error = None
|
|
try:
|
|
self.iDevices[fd].ungrab()
|
|
self.gDevices[fd] = False
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: ungrab device ("
|
|
+ str(self.iDevices[fd].name)
|
|
+ ")",
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
except IOError:
|
|
if self.gDevices[fd]:
|
|
return False
|
|
except Exception as ex:
|
|
ungrab_error = ex
|
|
|
|
if ungrab_error:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: ungrabing not possible: "
|
|
+ str(ungrab_error),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
def remove_device(self, fd):
|
|
"""Remove an input device from monitoring.
|
|
|
|
Cleanly removes a device by ungrabbing it, closing file handles,
|
|
and cleaning up all associated data structures.
|
|
|
|
Args:
|
|
fd (int): File descriptor of device to remove
|
|
|
|
Note:
|
|
Thread-safe operation with comprehensive error handling.
|
|
"""
|
|
with self._deviceLock:
|
|
try:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: device removed: "
|
|
+ str(fd)
|
|
+ " "
|
|
+ str(self.iDevices[fd]),
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"InputDriver evdev: device removed: "
|
|
+ str(fd)
|
|
+ " Error: "
|
|
+ str(e),
|
|
debug.DebugLevel.INFO,
|
|
)
|
|
self.clear_event_buffer()
|
|
try:
|
|
self.ungrab_device(fd)
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver remove_device: Error ungrabbing device "
|
|
+ str(fd)
|
|
+ ": "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
self.iDevices[fd].close()
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver remove_device: Error closing iDevice "
|
|
+ str(fd)
|
|
+ ": "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
self.uDevices[fd].close()
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver remove_device: Error closing uDevice "
|
|
+ str(fd)
|
|
+ ": "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
del self.iDevices[fd]
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver remove_device: Error deleting iDevice "
|
|
+ str(fd)
|
|
+ ": "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
del self.uDevices[fd]
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver remove_device: Error deleting uDevice "
|
|
+ str(fd)
|
|
+ ": "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
try:
|
|
del self.gDevices[fd]
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver remove_device: Error deleting gDevice "
|
|
+ str(fd)
|
|
+ ": "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
self.update_m_pi_devices_fd()
|
|
|
|
def has_i_devices(self):
|
|
if not self._initialized:
|
|
return False
|
|
if not self.iDevices:
|
|
return False
|
|
if len(self.iDevices) == 0:
|
|
return False
|
|
return True
|
|
|
|
def send_key(self, key, state):
|
|
"""Inject a synthetic key event.
|
|
|
|
Sends a key press or release event using UInput. Used for
|
|
features like key forwarding and macro execution.
|
|
|
|
Args:
|
|
key (str): Key name (e.g., 'KEY_A')
|
|
state (int): Key state (0=release, 1=press, 2=repeat)
|
|
"""
|
|
if not self._initialized:
|
|
return
|
|
try:
|
|
self.UInputinject.write(e.EV_KEY, e.ecodes[key], state)
|
|
self.UInputinject.syn()
|
|
except Exception as e:
|
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
"evdevDriver send_key: Error sending key "
|
|
+ str(key)
|
|
+ ": "
|
|
+ str(e),
|
|
debug.DebugLevel.ERROR,
|
|
)
|
|
|
|
def remove_all_devices(self):
|
|
if not self.has_i_devices():
|
|
return
|
|
devices = self.iDevices.copy()
|
|
for fd in devices:
|
|
self.remove_device(fd)
|
|
self.iDevices.clear()
|
|
self.uDevices.clear()
|
|
self.gDevices.clear()
|
|
self.iDeviceNo = 0
|