如何让这个“打字时禁用触摸板”的 Python 脚本更快地启用触摸板?

如何让这个“打字时禁用触摸板”的 Python 脚本更快地启用触摸板?

长话短说:我有一个带触摸板的键盘,它在 xinput 上被识别为“指针”,在 libinput 中具有“键盘指针”功能(与被识别为触摸板相反)。libinput 属性“Disable-w-typing”不可用(在“libinput list-devices”上它的值为“n/a”)。此外,Ubuntu 无法将其识别为触摸板,因此我无法使用 Ubuntu 嵌入式解决方案在打字时禁用触摸板。

通过阅读这里和其他地方的许多相关问题,我设法适应了这一点python 脚本这是我的问题。以下是我的版本:

import os
import time 
import subprocess
import threading

def main():
    touch = os.popen("xinput list --id-only 'pointer:SINO WEALTH USB KEYBOARD'").read()[:-1]
    keyboard = os.popen("xinput list --id-only 'keyboard:SINO WEALTH USB KEYBOARD'").read()[:-1]
    subprocess.call('xinput set-prop '+touch+' 142 1', shell=True)
    p = subprocess.Popen('xinput test '+keyboard, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    clickTime = [0, 0]
    def checkTime():
        keys = [37, 50, 62, 64, 105, 108, 133]
        while True:
            out = p.stdout.readline()
            if len(out) < 1:
                break
            key = int(out.split()[-1])
            if key not in keys:
                clickTime[0] = time.time()

    t = threading.Thread(target=checkTime)
    t.start()

    lastTime = 0
    touchpad = True
    while True:
        inactive = time.time() - clickTime[0]
        # print ('inactive for', inactive)
        if inactive > 1:            
            if not touchpad:
                print ('Enable touchpad')
                subprocess.call('xinput set-prop '+touch+' 142 1', shell=True)
            touchpad = True
        else:
            if touchpad:
                print ('Disable touchpad')
                subprocess.call('xinput set-prop '+touch+' 142 0', shell=True)
            touchpad = False
        time.sleep(0.5)

    retval = p.wait()

if __name__ == '__main__':
    main()

脚本运行良好。只要我开始输入,触摸板就会被禁用。唯一的问题是触摸板需要大约 1 秒才能重新启用,这有点长,而且我还没有找到缩短此延迟的方法。将“time.sleep(0.5)”设置为较小的数字似乎是一个显而易见的选择,但将其设置为 0.05(例如)似乎只会使脚本更加耗 CPU,但它不会对我停止输入和触摸板重新激活之间的延迟产生明显变化。

我的目标正是能够在打字时停用触摸板,并在停止打字后约 300 毫秒内重新激活触摸板。

我不一定需要使用 Python 来解决这个问题,但这是我能够解决这个问题的唯一方法。作为答案,我可以接受更改这个 Python 脚本的建议,或者可能是如何使用 Bash 脚本解决这个问题的指导,或者任何指导我解决这个问题的想法(也欢迎跳出思维定式)。

运行 Ubuntu 19.04。

答案1

编辑:

这可能是您问题的正确答案。我设法让它独立于按键重复设置工作,并且它比以前的版本更线程安全。我将保留另一个答案,并为使用 Wayland 的人提供纯 evdev 解决方案(当我有时间时)。

import time 
import subprocess
import threading
from queue import Queue, Empty

ENABLE = True # only for readability
DISABLE = False

TOUCHPAD_NAME = 'pointer:SINO WEALTH USB KEYBOARD'
KEYBOARD_NAME = 'keyboard:SINO WEALTH USB KEYBOARD'
DELAY = 0.3

def setDeviceEnabled(id, enabled):
    subprocess.call(['xinput', 'set-prop', str(id), 'Device Enabled', '1' if enabled else '0'])
    print('enabled' if enabled else 'disabled')

def main():
    touchpadId = subprocess.check_output(['xinput', 'list' , '--id-only', TOUCHPAD_NAME]).decode('UTF-8').strip()
    keyboardId = subprocess.check_output(['xinput', 'list' , '--id-only', KEYBOARD_NAME]).decode('UTF-8').strip()
    p = subprocess.Popen('xinput test ' + str(keyboardId), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    # we queue events to make sure the main thread doesn't miss consecutive events that happen too fast.
    eventQueue = Queue()

    def eventProducer():
        keysPressed = 0 # the number of keys currently pressed. We only need to enable/disable the touchpad if this transitions to/from 0, respectively.
        ignoreKeyCodes = [37, 50, 62, 64, 105, 108, 133]
        while True:
            out = p.stdout.readline()
            if len(out) < 1:
                break
            event = out.split()
            if not event[0] == b'key': # only react to key events. Enabling a real touchpad results in a "what's this" event on all input devices
                continue
            keyCode = int(event[2])
            if keyCode not in ignoreKeyCodes:
                if event[1] == b'press':
                    keysPressed += 1
                    if keysPressed == 1: # transition from 0 to 1 keys, disable touchpad
                        eventQueue.put([DISABLE])
                else:
                    keysPressed -= 1
                if keysPressed < 1: # transition from 1 to 0 keys, enable touchpad
                    keysPressed = 0 # in case we missed a press (e.g. a key was already pressed on startup), make sure this doesn't become negative
                    eventQueue.put([ENABLE, time.time()])

    t = threading.Thread(target=eventProducer)
    t.start()

    touchpadEnabled = True
    latestEvent = eventQueue.get()
    try:
        while True:
            if latestEvent[0] == DISABLE:
                if touchpadEnabled:
                    setDeviceEnabled(touchpadId, False)
                    touchpadEnabled = False
                latestEvent = eventQueue.get()
            else:
                timeToEnable = latestEvent[1] + DELAY - time.time()
                try:
                    latestEvent = eventQueue.get(timeout = timeToEnable) # Since the last event was ENABLE, the next event must be DISABLE. If it doesn't arrive until the timeout, we can enable the touchpad again.
                except Empty: # executed if no DISABLE event arrived until timeout
                    if not touchpadEnabled:
                        setDeviceEnabled(touchpadId, True)
                        touchpadEnabled = True
                    latestEvent = eventQueue.get()
    finally:
        # reenable the touchpad in any case
        setDeviceEnabled(touchpadId, True)

    retval = p.wait()

if __name__ == '__main__':
    main()

答案2

像当前脚本一样定期轮询非活动时间意味着触摸板将仅以特定频率启用/禁用。这会导致以下权衡:

  • 如果间隔太长,则启用或禁用触摸板的时间可能与间隔一样长,在您的情况下为半秒。
  • 以高频率(例如每毫秒)轮询会使脚本反应非常快,但会导致更高的 CPU 负载。

从概念上来说,您可以执行以下操作:

  • 等待键盘事件发生。
  • 禁用触摸板。
  • 根据最新的按键事件时间戳、当前系统时间以及您想要再次启用触摸板的延迟时间,您可以计算出何时需要再次检查按键事件并在此之前保持休眠状态。如果在此时间之后没有发生其他按键事件,则启用触摸板。否则,计算何时必须再次检查并重复此步骤。

例如:在 0ms 时按下了一个键。您想在 350ms 后再次启用触摸板,因此您知道可以休眠 350ms。当您唤醒时,您看到在 250ms 时按下了另一个键。根据该时间戳、当前系统时间 (350ms) 和指定的延迟 (350ms),您现在知道需要在最后一个按键事件发生后 350ms(即 600ms 时)进行检查,因此您可以再次休眠 250ms。

这样,您可以确保在按下某个键时立即禁用触摸板,并在释放最后一个键后非常接近 350 毫秒再次启用,而无需以高频率进行轮询。


此脚本用于python-evdev读取按键事件。这样做的好处是我们不必自己进行任何线程处理,我们可以使用 轻松等待按键事件selector。缺点是该脚本需要键盘 evdev 设备节点的读取权限,因此必须以 root 身份运行(除非您想将用户添加到输入组或通过 udev 规则更改权限)。

运行sudo apt install python3-evdev以安装python-evdevpython3。将KEYBOARD_NAMETOUCHPAD_NAME和更改DELAY为所需值:

#!/bin/env python3

import subprocess
import time
import evdev
from evdev import ecodes
import selectors
from selectors import DefaultSelector, EVENT_READ


DELAY = 0.35 # time in seconds after which the touchpad will be enabled again

KEYBOARD_NAME = "SINO WEALTH USB KEYBOARD" # the name as shown by evtest
TOUCHPAD_NAME = "pointer:SINO WEALTH USB KEYBOARD" # the name as shown by xinput list

lastKeyPress = 0
touchpadDisabled = False
touchpadId = -1

ignoreKeycodes = [ecodes.KEY_LEFTCTRL, ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT, ecodes.KEY_LEFTALT, ecodes.KEY_RIGHTCTRL, ecodes.KEY_RIGHTALT, ecodes.KEY_LEFTMETA]

def getKeyboard():
    for device in evdev.list_devices():
        evdevDevice = evdev.InputDevice(device)
        if evdevDevice.name == KEYBOARD_NAME:
            # If touchpad and keyboard have the same name, check if the device has an ESC key (which a touchpad probably doesn't have)
            caps = evdevDevice.capabilities()
            if ecodes.EV_KEY in caps:
                if ecodes.KEY_ESC in caps[ecodes.EV_KEY]:
                    return evdevDevice
        evdevDevice.close()
    raise OSError("Unable to find keyboard: " + KEYBOARD_NAME)

def updateLastKeypress(event):
    global lastKeyPress
    if event.type == ecodes.EV_KEY:
        if not event.code in ignoreKeycodes:
            lastKeyPress = event.timestamp()

def enableTouchpad(force=False):
    global touchpadDisabled, touchpadId
    if touchpadDisabled or force:
        process = subprocess.run(["xinput", "set-prop", str(touchpadId), "143", "1"])
        touchpadDisabled = False

def disableTouchpad(force=False):
    global touchpadDisabled, touchpadId
    if not touchpadDisabled or force:
        process = subprocess.run(["xinput", "set-prop", str(touchpadId), "143", "0"])
        touchpadDisabled = True

def main():
    global touchpadId
    keyboard = getKeyboard()
    touchpadId = subprocess.check_output(["xinput", "list" , "--id-only", TOUCHPAD_NAME]).decode("UTF-8").strip() # this will raise an exception if it fails since xinput will exit with non-zero status.
    selector = selectors.DefaultSelector()
    selector.register(keyboard, selectors.EVENT_READ)

    while True:
        enableTouchpad()
        for key, mask in selector.select(): # this is where we wait for key events. Execution blocks until an event is available.
            device = key.fileobj

            while True: # we will stay in this loop until we can enable the touchpad again
                try:
                    for event in device.read():
                        updateLastKeypress(event)
                except BlockingIOError: # this will be raised by device.read() if there is no more event to read
                    pass
                timeToSleep = (lastKeyPress + DELAY) - time.time()
                if timeToSleep <= 0.005: # you can set this to 0, but that may result in unnecessarily short (and imperceptible) sleep times.
                    # touchpad can be enabled again, so break loop.
                    break
                else:
                    # disable touchpad and wait until we need to check next. disableTouchpad() takes care of only invoking xinput if necessary.
                    disableTouchpad()
                    time.sleep(timeToSleep)

if __name__ == "__main__":
    try:
        main()
    except:
        # make sure the touchpad is enabled again when any error occurs
        enableTouchpad(force=True)
        raise

相关内容