# python-keyboard 按键扫描优化

从 2020 年开始,主力键盘从宁芝静电容换成了使用 python-keyboard (粉笔套 + 佳达隆白轴按键)。前两年使用下来没有遇到什么问题,直到最近两个月开始,有几个常用按键开始出现误触的情况。猜测可能是机械按键老化性能下降导致抖动加剧,进而影响了键值识别

打开控制台,跟踪了一下日志,发现单次按键按下被识别成了两次按下。

key 07
7 7 \ 0x24 latency 7 | 759
key 87
7 7 / 0x24 latency 9 | 25
key 07
7 7 \ 0x24 latency 7 | 21
key 87
7 7 / 0x24 latency 9 | 25
key 0D

跟踪代码发现跟这个函数的实现有关:

https://github.com/makerdiary/python-keyboard/blob/main/keyboard/matrix.py#L50

    def scan(self):
        """
        Scan keyboard matrix and save key event into the queue.
        :return: length of the key event queue.
        """
        t = time.monotonic_ns()

        # use local variables to speed up
        pressed = self.pressed
        last_mask = self.mask
        cols = self.cols

        mask = 0
        count = 0
        key_index = -1
        for row in self.rows:
            row.value = pressed  # select row
            for col in cols:
                key_index += 1
                if col.value == pressed:
                    key_mask = 1 << key_index
                    if not (last_mask & key_mask):
                        if t - self.t1[key_index] < self._debounce_time:
                            print("debonce")
                            continue

                        self.t0[key_index] = t
                        self.put(key_index)

                    mask |= key_mask
                    count += 1
                elif last_mask and (last_mask & (1 << key_index)):
                    if t - self.t0[key_index] < self._debounce_time:
                        print("debonce")
                        mask |= 1 << key_index
                        continue

                    self.t1[key_index] = t
                    self.put(0x80 | key_index)

            row.value = not pressed
        self.mask = mask
        self.count = count

        return self.length

上面函数消抖部分处理有问题,按下消抖确认使用释放时的时间戳(t1)不合理。扫描部分的逻辑耦合较为严重。新的思路是把矩阵键盘上的每个按键进行独立检测(硬件上已经支持),并利用状态机重构scan函数。

class MatrixKey:
    def __init__(self) -> None:
        self.sta = 0
        self.ts = 0
        self.ts_pressed = 0
        self.ts_released = 0

    # return value: 1 pressed, 0 released, -1 wait/idle
    def scan(self, ts, pressed, duration) -> int:
        if self.sta not in [0, 2] and ts - self.ts < duration:
            return -1
        self.ts = ts

        if self.sta == 0:
            if pressed:
                # press detected, to debounce
                self.sta = 1
        elif self.sta == 1:
            if not pressed:
                # too short, ignore
                self.sta = 0
            else:
                # press confirm
                self.sta = 2
                self.ts_pressed = ts
                return 1
        elif self.sta == 2:
            if not pressed:
                # release detected, to debounce
                self.sta = 3
        elif self.sta == 3:
            if pressed:
                # too short, ignore
                self.sta = 0
            else:
                # release confirm
                self.sta = 0
                self.ts_released = ts
                return 0
        return -1
    
class Matrix:    
    def __init__(self):
        ...
        self._keys = []
        for _ in range(self.keynum):
            self._keys.append(MatrixKey())        
    
    def _scan(self):
        ts = time.monotonic_ns()
        pressed = self.pressed
        cols = self.cols
        rows = self.rows
        key_index = -1
        for row in rows:
            row.value = pressed  # select row
            for col in cols:
                key_index += 1
                k = self._keys[key_index].scan(ts, col.value == pressed, self._debounce_time)
                if k == 1:
                    self.put(key_index)
                elif k == 0:
                    self.put(0x80 | key_index)
            row.value = not pressed

在嵌入式这个领域,很多确实是硬件问题的问题,其实可能是可以使用软件来解决的。这也确实很难,需要设计人员有未卜先知的能力。

代码仓库:
https://github.com/JiapengLi/python-keyboard