Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the HID Protocol. #1693

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions doc/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ There are two ways to install plugins:
Plover comes with the following machine plugins installed:

- Keyboard
- HID
- Gemini PR
- TX Bolt
- Passport
Expand Down
173 changes: 173 additions & 0 deletions plover/machine/hid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'''
A plover machine plugin for supporting the Plover HID protocol.

This protocol is a simple HID-based protocol that sends the current state
of the steno machine every time that state changes.

See the README for more details on the protocol.

The order of the buttons (from left to right) is the same as in `KEYS_LAYOUT`.
Most buttons have the same names as in GeminiPR, except for the extra buttons
which are called X1-X26.
'''

from plover.machine.base import ThreadedStenotypeBase
from plover import log
from plover.misc import boolean

import hid
import platform
import time

# This is a hack to not open the hid device in exclusive mode on
# darwin, if the version of hidapi installed is current enough
if platform.system() == "Darwin":
import ctypes
try:
hid.hidapi.hid_darwin_set_open_exclusive.argtypes = (ctypes.c_int, )
hid.hidapi.hid_darwin_set_open_exclusive.restype = None
hid.hidapi.hid_darwin_set_open_exclusive(0)
except AttributeError as e:
log.error("hidapi < 0.12 in use, plover-hid will not work correctly")

USAGE_PAGE: int = 0xFF50
USAGE: int = 0x4C56

N_LEVERS: int = 64

# A simple report contains the report id 1 and one bit
# for each of the 64 buttons in the report.
SIMPLE_REPORT_TYPE: int = 0x01
SIMPLE_REPORT_LEN: int = N_LEVERS // 8

class InvalidReport(Exception):
pass

STENO_KEY_CHART = ("S-", "T-", "K-", "P-", "W-", "H-",
"R-", "A-", "O-", "*", "-E", "-U",
"-F", "-R", "-P", "-B", "-L", "-G",
"-T", "-S", "-D", "-Z", "#",
"X1", "X2", "X3", "X4", "X5", "X6",
"X7", "X8", "X9", "X10", "X11", "X12",
"X13", "X14", "X15", "X16", "X17", "X18",
"X19", "X20", "X21", "X22", "X23", "X24",
"X25", "X26", "X27", "X28", "X29", "X30",
"X31", "X32", "X33", "X34", "X35", "X36",
"X37", "X38", "X39", "X40", "X41")

class HidMachine(ThreadedStenotypeBase):
KEYS_LAYOUT: str = '''
# # # # # # # # # #
S- T- P- H- * -F -P -L -T -D
S- K- W- R- * -R -B -G -S -Z
A- O- -E -U
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10
X11 X12 X13 X14 X15 X16 X17 X18 X19 X20
X21 X22 X23 X24 X25 X26 X27 X28 X29 X30
X31 X32 X33 X34 X35 X36 X37 X38 X39 X40
X41
'''
def __init__(self, params):
super().__init__()
self._params = params
self._hid = None

def _parse(self, report):
# The first byte is the report id, and due to idiosynchrasies
# in how HID-apis work on different operating system we can't
# map the report id to the contents in a good way, so we force
# compliant devices to always use a report id of 0x50 ('P').
if len(report) > SIMPLE_REPORT_LEN and report[0] == 0x50:
return int.from_bytes(report[1:SIMPLE_REPORT_LEN+1], 'big')
else:
raise InvalidReport()

def send(self, keystate):
steno_actions = self.keymap.keys_to_actions(
[key for i, key in enumerate(STENO_KEY_CHART) if keystate >> (63 - i) & 1]
)
if steno_actions:
self._notify(steno_actions)

def run(self):
self._ready()
keystate = 0
current = 0
last_sent = 0
press_started = time.time()
sent_first_up = False
while not self.finished.wait(0):
interval_ms = self._params["repeat_interval_ms"]
try:
report = self._hid.read(65536, timeout=interval_ms)
except hid.HIDException:
self._error()
return
if not report:
# The set of keys pressed down hasn't changed. Figure out if we need to be sending repeats:
if self._params["double_tap_repeat"] and 0 != current == last_sent and time.time() - press_started > self._params["repeat_delay_ms"] / 1e3:
self.send(current)
# Avoid sending an extra chord when the repeated chord is released.
sent_first_up = True
continue
try:
current = self._parse(report)
except InvalidReport:
continue

press_started = time.time()
if self._params["first_up_chord_send"]:
if keystate & ~current and not sent_first_up:
# A finger went up: send a first-up chord and remember it.
self.send(keystate)
last_sent = keystate
sent_first_up = True
if current & ~keystate:
# A finger went down: get ready to send a new first-up chord.
sent_first_up = False
keystate = current
else:
keystate |= current
if current == 0:
# All fingers are up: send the "total" chord and reset it.
self.send(keystate)
last_sent = keystate
keystate = 0

def start_capture(self):
self.finished.clear()
self._initializing()
# Enumerate all hid devices on the machine and if we find one with our
# usage page and usage we try to connect to it.
try:
devices = [
device["path"]
for device in hid.enumerate()
if device["usage_page"] == USAGE_PAGE and device["usage"] == USAGE
]
if not devices:
self._error()
return
# FIXME: if multiple compatible devices are found we should either
# let the end user configure which one they want, or support reading
# from all connected plover hid devices at the same time.
self._hid = hid.Device(path=devices[0])
except hid.HIDException:
self._error()
return
self.start()

def stop_capture(self):
super().stop_capture()
if self._hid:
self._hid.close()
self._hid = None

@classmethod
def get_option_info(cls):
return {
"first_up_chord_send": (False, boolean),
"double_tap_repeat": (False, boolean),
"repeat_delay_ms": (200, int),
"repeat_interval_ms": (50, int),
}
26 changes: 26 additions & 0 deletions plover/system/english_stenotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,32 @@
ORTHOGRAPHY_WORDLIST = 'american_english_words.txt'

KEYMAPS = {
'HID': {
'#' : '#',
'S-' : 'S-',
'T-' : 'T-',
'K-' : 'K-',
'P-' : 'P-',
'W-' : 'W-',
'H-' : 'H-',
'R-' : 'R-',
'A-' : 'A-',
'O-' : 'O-',
'*' : '*',
'-E' : '-E',
'-U' : '-U',
'-F' : '-F',
'-R' : '-R',
'-P' : '-P',
'-B' : '-B',
'-L' : '-L',
'-G' : '-G',
'-T' : '-T',
'-S' : '-S',
'-D' : '-D',
'-Z' : '-Z',
'no-op' : ("X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8", "X9", "X10", "X11", "X12", "X13", "X14", "X15", "X16", "X17", "X18", "X19", "X20", "X21", "X22", "X23", "X24", "X25", "X26", "X27", "X28", "X29", "X30", "X31", "X32", "X33", "X34", "X35", "X36", "X37", "X38", "X39", "X40", "X41"),
},
'Gemini PR': {
'#' : ('#1', '#2', '#3', '#4', '#5', '#6', '#7', '#8', '#9', '#A', '#B', '#C'),
'S-' : ('S1-', 'S2-'),
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ plover.gui.qt.tool =
paper_tape = plover.gui_qt.paper_tape:PaperTape
suggestions = plover.gui_qt.suggestions_dialog:SuggestionsDialog
plover.machine =
HID = plover.machine.hid:HidMachine
Gemini PR = plover.machine.geminipr:GeminiPr
Keyboard = plover.machine.keyboard:Keyboard
Passport = plover.machine.passport:Passport
Expand Down