-
-
Notifications
You must be signed in to change notification settings - Fork 652
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add original Seika Notetaker driver (#11514)
Seika Notetaker drivers did not exist in NVDA The Seika Notetaker Braille display is not the same as the Seika Braille display. Seika Notetaker has 8 dots, Braille keyboard and other function keys/buttons
- Loading branch information
Showing
2 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,311 @@ | ||
# A part of NonVisual Desktop Access (NVDA) | ||
# Copyright (C) 2012-2021 NV Access Limited, Ulf Beckmann <beckmann@flusoft.de> | ||
# This file may be used under the terms of the GNU General Public License, version 2 or later. | ||
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html | ||
# | ||
# This file represents the braille display driver for | ||
# Seika Notetaker, a product from Nippon Telesoft | ||
# see www.seika-braille.com for more details | ||
|
||
from io import BytesIO | ||
import typing | ||
from typing import List | ||
|
||
import braille | ||
import brailleInput | ||
import inputCore | ||
import hwPortUtils | ||
import bdDetect | ||
import hwIo | ||
from serial.win32 import INVALID_HANDLE_VALUE | ||
from logHandler import log | ||
|
||
MAX_READ_ATTEMPTS = 30 | ||
READ_TIMEOUT_SECS = 0.2 | ||
|
||
DOT_1 = 0x1 | ||
DOT_2 = 0x2 | ||
DOT_3 = 0x4 | ||
DOT_4 = 0x8 | ||
DOT_5 = 0x10 | ||
DOT_6 = 0x20 | ||
DOT_7 = 0x40 | ||
DOT_8 = 0x80 | ||
|
||
|
||
_keyNames = { | ||
0x000001: "BACKSPACE", | ||
0x000002: "SPACE", | ||
0x000004: "LB", | ||
0x000008: "RB", | ||
0x000010: "LJ_CENTER", | ||
0x000020: "LJ_LEFT", | ||
0x000040: "LJ_RIGHT", | ||
0x000080: "LJ_UP", | ||
0x000100: "LJ_DOWN", | ||
0x000200: "RJ_CENTER", | ||
0x000400: "RJ_LEFT", | ||
0x000800: "RJ_RIGHT", | ||
0x001000: "RJ_UP", | ||
0x002000: "RJ_DOWN" | ||
} | ||
|
||
SEIKA_REQUEST_INFO = b"\x03\xff\xff\xa1" | ||
SEIKA_INFO = b"\xff\xff\xa2" | ||
SEIKA_SEND_TEXT = b"\x2c\xff\xff\xa3" | ||
SEIKA_ROUTING = b"\xff\xff\xa4" | ||
SEIKA_KEYS = b"\xff\xff\xa6" | ||
SEIKA_KEYS_ROU = b"\xff\xff\xa8" | ||
|
||
SEIKA_CONFIG = b"\x50\x00\x00\x25\x80\x00\x00\x03\x00" | ||
SEIKA_CMD_ON = b"\x41\x01" | ||
|
||
vidpid = "VID_10C4&PID_EA80" | ||
hidvidpid = "HID\\VID_10C4&PID_EA80" | ||
SEIKA_NAME = "seikantk" | ||
|
||
|
||
def _getDotNames(): | ||
dotNames = {} | ||
for dotNum in range(1, 9): | ||
keyName = globals()[f"DOT_{dotNum}"] | ||
dotNames[keyName] = f"d{dotNum}" | ||
return dotNames | ||
|
||
|
||
_dotNames = _getDotNames() | ||
bdDetect.addUsbDevices(SEIKA_NAME, bdDetect.KEY_HID, {vidpid, }) | ||
|
||
|
||
class BrailleDisplayDriver(braille.BrailleDisplayDriver): | ||
_dev: hwIo.IoBase | ||
name = SEIKA_NAME | ||
# Translators: Name of a braille display. | ||
description = _("Seika Notetaker") | ||
path = "" | ||
isThreadSafe = True | ||
for d in hwPortUtils.listHidDevices(): | ||
if d["hardwareID"].startswith(hidvidpid): | ||
path = d["devicePath"] | ||
|
||
@classmethod | ||
def check(cls): | ||
return True | ||
|
||
@classmethod | ||
def getManualPorts(cls): | ||
return cls.path | ||
|
||
def __init__(self, port="hid"): | ||
super().__init__() | ||
self.numCells = 0 | ||
self.numBtns = 0 | ||
self.handle = None | ||
|
||
self._hidBuffer = b"" | ||
self._command: typing.Optional[bytes] = None | ||
self._argsLen: typing.Optional[int] = None | ||
log.info(f"Seika Notetaker braille driver path: {self.path}") | ||
|
||
if self.path == "": | ||
raise RuntimeError("No MINI-SEIKA display found, no path found") | ||
self._dev = dev = hwIo.Hid(path=self.path, onReceive=self._onReceive) | ||
if dev._file == INVALID_HANDLE_VALUE: | ||
raise RuntimeError("No MINI-SEIKA display found, open error") | ||
dev.setFeature(SEIKA_CONFIG) # baud rate, stop bit usw | ||
dev.setFeature(SEIKA_CMD_ON) # device on | ||
dev.write(SEIKA_REQUEST_INFO) # Request the Info from the device | ||
|
||
# wait and try to get info from the Braille display | ||
for i in range(MAX_READ_ATTEMPTS): # the info-block is about | ||
dev.waitForRead(READ_TIMEOUT_SECS) | ||
if self.numCells: | ||
log.info( | ||
f"Seika notetaker on USB-HID," | ||
f" Cells {self.numCells}" | ||
f" Buttons {self.numBtns}" | ||
) | ||
break | ||
|
||
if self.numCells == 0: | ||
dev.close() | ||
raise RuntimeError("No MINI-SEIKA display found, no response") | ||
|
||
def terminate(self): | ||
try: | ||
super().terminate() | ||
finally: | ||
self._dev.close() | ||
|
||
def display(self, cells: List[int]): | ||
# cells will already be padded up to numCells. | ||
cellBytes = SEIKA_SEND_TEXT + self.numCells.to_bytes(1, 'little') + bytes(cells) | ||
self._dev.write(cellBytes) | ||
|
||
def _onReceive(self, data: bytes): | ||
""" | ||
Note: Further insight into this function would be greatly appreciated. | ||
This function is a very simple state machine, each stage represents the collection of a field, when all | ||
fields are collected the command they represent can be processed. | ||
On each call to _onReceive three bytes are read from the device. | ||
The first and third bytes are discarded, the second byte is appended to a buffer. | ||
The buffer is accumulated until the buffer has the required number of bytes for the field being collected. | ||
There are 3 fields to be collected before a command can be processed: | ||
1: first 3 bytes: command | ||
2: 1 byte: specify total length in bytes? | ||
3: variable length: arguments for command type | ||
After accumulating enough bytes for each phase, the buffer is cleared and the next stage is entered. | ||
""" | ||
COMMAND_LEN = 3 | ||
stream = BytesIO(data) | ||
cmd = stream.read(3) # Note, first and third bytes are discarded | ||
newByte: bytes = cmd[1:2] # use range to return bytes | ||
self._hidBuffer += newByte | ||
hasCommandBeenCollected = self._command is None | ||
hasArgLenBeenCollected = self._argsLen is None | ||
if ( # still collecting command bytes | ||
not hasCommandBeenCollected | ||
and len(self._hidBuffer) == COMMAND_LEN | ||
): | ||
self._command = self._hidBuffer # command found reset and wait for args length | ||
self._hidBuffer = None | ||
elif ( # next byte gives the command + args length | ||
hasCommandBeenCollected | ||
and not hasArgLenBeenCollected # argsLen has not | ||
): | ||
# Unknown why we must wait for 4 extra bytes. Without a device to inspect actual data | ||
# it has to be assumed that the prior approach is correct, and infer what we can from | ||
# it. | ||
# Best guess: the data is sent with the following structure | ||
# - command name (3 bytes) | ||
# - total bytes Command + Args size (1 byte) | ||
# - Args (variable bytes) | ||
# - Constant 4 bytes containing unknown | ||
self._argsLen = ord(newByte) - COMMAND_LEN + 4 | ||
# don't reset _hidBuffer the value for total length | ||
elif ( # now collect the args, | ||
hasCommandBeenCollected | ||
and hasArgLenBeenCollected | ||
and len(self._hidBuffer) == self._argsLen | ||
): | ||
arg = self._hidBuffer | ||
command = self._command | ||
|
||
# reset state variables | ||
self._command = None | ||
self._argsLen = None | ||
self._hidBuffer = b"" | ||
self._processCommand(command, arg) | ||
|
||
def _processCommand(self, command: bytes, arg: bytes) -> None: | ||
if command == SEIKA_INFO: | ||
self._handInfo(arg) | ||
elif command == SEIKA_ROUTING: | ||
self._handRouting(arg) | ||
elif command == SEIKA_KEYS: | ||
self._handKeys(arg) | ||
elif command == SEIKA_KEYS_ROU: | ||
self._handKeysRouting(arg) | ||
else: | ||
log.warning(f"Seika device has received an unknown command {command}") | ||
|
||
def _handInfo(self, arg: bytes): | ||
self.numCells = arg[2] | ||
self.numBtns = arg[1] | ||
|
||
def _handRouting(self, arg: bytes): | ||
for i in range(arg[0]): | ||
for j in range(8): | ||
if arg[i + 1] & (1 << j): | ||
routingIndex = i * 8 + j | ||
gesture = InputGestureRouting(routingIndex) | ||
try: | ||
inputCore.manager.executeGesture(gesture) | ||
except inputCore.NoInputGestureAction: | ||
log.debug("No action for Seika Notetaker routing command") | ||
|
||
def _handKeys(self, arg: bytes): | ||
brailleDots = arg[1] | ||
key = arg[2] | (arg[3] << 8) | ||
gesture = None | ||
if key: # Mini Seika has 2 Top and 4 Front | ||
gesture = InputGesture(keys=key) | ||
if brailleDots: | ||
gesture = InputGesture(dots=brailleDots) | ||
if gesture is not None: | ||
try: | ||
inputCore.manager.executeGesture(gesture) | ||
except inputCore.NoInputGestureAction: | ||
log.debug("No action for Seika Notetaker keys.") | ||
|
||
def _handKeysRouting(self, arg: bytes): | ||
argk = b"\x03" + arg[1:] | ||
argr = (arg[0] - 3).to_bytes(1, 'little') + arg[4:] | ||
self._handRouting(argr) | ||
self._handKeys(argk) | ||
|
||
gestureMap = inputCore.GlobalGestureMap({ | ||
"globalCommands.GlobalCommands": { | ||
"braille_routeTo": ("br(seikantk):routing",), | ||
"braille_scrollBack": ("br(seikantk):LB",), | ||
"braille_scrollForward": ("br(seikantk):RB",), | ||
"braille_previousLine": ("br(seikantk):LJ_UP",), | ||
"braille_nextLine": ("br(seikantk):LJ_DOWN",), | ||
"braille_toggleTether": ("br(seikantk):LJ_CENTER",), | ||
"sayAll": ("br(seikantk):SPACE+BACKSPACE",), | ||
"showGui": ("br(seikantk):RB+LB",), | ||
"kb:tab": ("br(seikantk):LJ_RIGHT",), | ||
"kb:shift+tab": ("br(seikantk):LJ_LEFT",), | ||
"kb:upArrow": ("br(seikantk):RJ_UP",), | ||
"kb:downArrow": ("br(seikantk):RJ_DOWN",), | ||
"kb:leftArrow": ("br(seikantk):RJ_LEFT",), | ||
"kb:rightArrow": ("br(seikantk):RJ_RIGHT",), | ||
"kb:shift+upArrow": ("br(seikantk):SPACE+RJ_UP", "br(seikantk):BACKSPACE+RJ_UP"), | ||
"kb:shift+downArrow": ("br(seikantk):SPACE+RJ_DOWN", "br(seikantk):BACKSPACE+RJ_DOWN"), | ||
"kb:shift+leftArrow": ("br(seikantk):SPACE+RJ_LEFT", "br(seikantk):BACKSPACE+RJ_LEFT"), | ||
"kb:shift+rightArrow": ("br(seikantk):SPACE+RJ_RIGHT", "br(seikantk):BACKSPACE+RJ_RIGHT"), | ||
"kb:escape": ("br(seikantk):SPACE+RJ_CENTER",), | ||
"kb:windows": ("br(seikantk):BACKSPACE+RJ_CENTER",), | ||
"kb:space": ("br(seikantk):BACKSPACE", "br(seikantk):SPACE",), | ||
"kb:backspace": ("br(seikantk):d7",), | ||
"kb:pageup": ("br(seikantk):SPACE+LJ_RIGHT",), | ||
"kb:pagedown": ("br(seikantk):SPACE+LJ_LEFT",), | ||
"kb:home": ("br(seikantk):SPACE+LJ_UP",), | ||
"kb:end": ("br(seikantk):SPACE+LJ_DOWN",), | ||
"kb:control+home": ("br(seikantk):BACKSPACE+LJ_UP",), | ||
"kb:control+end": ("br(seikantk):BACKSPACE+LJ_DOWN",), | ||
"kb:enter": ("br(seikantk):RJ_CENTER", "br(seikantk):d8"), | ||
}, | ||
}) | ||
|
||
|
||
class InputGestureRouting(braille.BrailleDisplayGesture): | ||
source = BrailleDisplayDriver.name | ||
|
||
def __init__(self, index): | ||
super().__init__() | ||
self.id = "routing" | ||
self.routingIndex = index | ||
|
||
|
||
class InputGesture(braille.BrailleDisplayGesture, brailleInput.BrailleInputGesture): | ||
source = BrailleDisplayDriver.name | ||
|
||
def __init__(self, keys=None, dots=None, space=False, routing=None): | ||
super(braille.BrailleDisplayGesture, self).__init__() | ||
# see what thumb keys are pressed: | ||
names = set() | ||
if keys is not None: | ||
names.update(_keyNames[1 << i] for i in range(22) if (1 << i) & keys) | ||
elif dots is not None: | ||
self.dots = dots | ||
if space: | ||
self.space = space | ||
names.add(_keyNames[1]) | ||
names.update(_dotNames[1 << i] for i in range(8) if (1 << i) & dots) | ||
elif routing is not None: | ||
self.routingIndex = routing | ||
names.add('routing') | ||
self.id = "+".join(names) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters