175 lines
6.7 KiB
Python
175 lines
6.7 KiB
Python
'''
|
|
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.
|
|
|
|
This gives us two things compared to using a serial protocol, first of all the
|
|
same protocol can be used when using both USB and bluetooth, secondly we can do
|
|
all of the logic for things like first up chord send, and repeat on the plover
|
|
side instead of having to rewrite firmware for all of the different hobbyist
|
|
machines.
|
|
|
|
The protocol is a standard HID protocol with the following HID descriptor:
|
|
{
|
|
0x06, 0x50, 0xff, // UsagePage (65360)
|
|
0x0a, 0x56, 0x4c, // Usage (19542)
|
|
0xa1, 0x02, // Collection (Logical)
|
|
0x85, 0x01, // ReportID (1)
|
|
0x25, 0x01, // LogicalMaximum (1)
|
|
0x75, 0x01, // ReportSize (1)
|
|
0x95, 0x40, // ReportCount (64)
|
|
0x05, 0x09, // UsagePage (button)
|
|
0x19, 0x00, // UsageMinimum (Button(0))
|
|
0x29, 0x3f, // UsageMaximum (Button(63))
|
|
0x81, 0x02, // Input (Variable)
|
|
0x85, 0x02, // ReportID (2)
|
|
0x26, 0xff, 0x00, // LogicalMaximum (255)
|
|
0x75, 0x08, // ReportSize (8)
|
|
0x19, 0x00, // UsageMinimum (Button(0))
|
|
0x29, 0x3f, // UsageMaximum (Button(63))
|
|
0x81, 0x02, // Input (Variable)
|
|
0xc0, // EndCollection
|
|
}
|
|
|
|
The descriptor is generated by the HID Report Descriptor Compiler (https://github.com/nipo/hrdc) from
|
|
the following python description:
|
|
```
|
|
stenomachine = Collection(Collection.Logical, Usage('vendor-defined', 0xFF504C56),
|
|
Report(1, *(Value(Value.Input, button.Button(x), 1, logicalMin=0, logicalMax=1) for x in range(64))),
|
|
Report(2, *(Value(Value.Input, button.Button(x), 8, logicalMin=0, logicalMax=255) for x in range(64))))
|
|
```
|
|
|
|
The usage page is a vendor-defined usage page (starts with 0xFF), the following
|
|
bytes of the usage page and usage are set to the ascii values of the text
|
|
string "STN".
|
|
|
|
This hid-descriptor defines two types of reports, the first one being what we
|
|
call the simple report, which defines 64 buttons with one bit for each button,
|
|
a one signifying that the button is pressed and a zero that the button in that
|
|
position isn't pressed.
|
|
|
|
The second kind of report is what we call a complicated report, that one uses
|
|
one byte for each button, and allows for things such as sending lever pressure
|
|
information, a zero signifying no pressure on the lever, and 255 signifying
|
|
maximum pressure.
|
|
|
|
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 bitstring import BitString
|
|
import hid
|
|
|
|
USAGE_PAGE: int = 0xFF50
|
|
USAGE: int = 0x4C56
|
|
|
|
N_BUTTONS: 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 = 1 + N_BUTTONS // 8
|
|
|
|
# A complicated report contains the report id 2 and one byte
|
|
# for each of the 64 buttons in the report.
|
|
COMPLICATED_REPORT_TYPE = 0x02
|
|
COMPLICATED_REPORT_LEN = 1 + N_BUTTONS
|
|
|
|
class InvalidReport(Exception):
|
|
pass
|
|
|
|
class HidMachine(ThreadedStenotypeBase):
|
|
KEYS_LAYOUT: str = """
|
|
#1 #2 #3 #4 #5 #6 #7 #8 #9 #A #B #C
|
|
X1 S1- T- P- H- *1 *3 -F -P -L -T -D
|
|
X2 S2- K- W- R- *2 *4 -R -B -G -S -Z
|
|
X3 A- O- -E -U X4
|
|
|
|
X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15
|
|
X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26
|
|
"""
|
|
STENO_KEY_MAP: [str] = KEYS_LAYOUT.split()
|
|
|
|
def __init__(self, params):
|
|
super().__init__()
|
|
self._params = params
|
|
self._hid = None
|
|
# FIXME: this should be configurable by the end user
|
|
self._tresholds = [0.5]*N_BUTTONS
|
|
|
|
def _parse(self, report):
|
|
# Windows will always return the largest report size (padded with zeros).
|
|
if report[0] == SIMPLE_REPORT_TYPE and len(report) >= SIMPLE_REPORT_LEN:
|
|
return BitString(report[1:SIMPLE_REPORT_LEN])
|
|
elif (
|
|
report[0] == COMPLICATED_REPORT_TYPE
|
|
and len(report) == COMPLICATED_REPORT_LEN
|
|
):
|
|
# Scale the key pressure information to a float between 0 and 1 and
|
|
# compare it to the tresholds to see if it should count as a key
|
|
# press or not.
|
|
return BitString(
|
|
[
|
|
pressure / 256 >= treshold
|
|
for (pressure, threshold) in zip(report[1:], self._tresholds)
|
|
]
|
|
)
|
|
else:
|
|
raise InvalidReport()
|
|
|
|
def run(self):
|
|
self._ready()
|
|
keystate = BitString(N_BUTTONS)
|
|
while not self.finished.wait(0):
|
|
try:
|
|
report = self._hid.read(65536, timeout=1000)
|
|
except hid.HIDException:
|
|
self._error()
|
|
return
|
|
if not report:
|
|
continue
|
|
report = self._parse(report)
|
|
keystate |= report
|
|
if not report:
|
|
steno_actions = self.keymap.keys_to_actions(
|
|
[self.STENO_KEY_MAP[i] for (i, x) in enumerate(keystate) if x]
|
|
)
|
|
if steno_actions:
|
|
self._notify(steno_actions)
|
|
keystate = BitString(N_BUTTONS)
|
|
|
|
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 {}
|