Skip to content
Merged
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
137 changes: 72 additions & 65 deletions source/brailleDisplayDrivers/seikantk.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from io import BytesIO
import typing
from typing import List
from typing import Dict, List, Set

import braille
import brailleInput
Expand All @@ -23,16 +23,6 @@
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",
Expand All @@ -47,7 +37,17 @@
0x000400: "RJ_LEFT",
0x000800: "RJ_RIGHT",
0x001000: "RJ_UP",
0x002000: "RJ_DOWN"
0x002000: "RJ_DOWN",
}
_dotNames = {
0x1: "d1",
0x2: "d2",
0x4: "d3",
0x8: "d4",
0x10: "d5",
0x20: "d6",
0x40: "d7",
0x80: "d8",
}

SEIKA_REQUEST_INFO = b"\x03\xff\xff\xa1"
Expand All @@ -64,16 +64,6 @@
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, })


Expand All @@ -100,6 +90,7 @@ def __init__(self, port="hid"):
super().__init__()
self.numCells = 0
self.numBtns = 0
self.numRoutingKeys = 0
self.handle = None

self._hidBuffer = b""
Expand Down Expand Up @@ -153,7 +144,7 @@ def _onReceive(self, data: bytes):
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?
2: 1 byte: specify length of subsequent arguments 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.
Expand All @@ -175,16 +166,12 @@ def _onReceive(self, data: bytes):
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
# the data is sent with the following structure
# - command name (3 bytes)
# - total bytes Command + Args size (1 byte)
# - number of subsequent bytes to read (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
self._argsLen = ord(newByte)
self._hidBuffer = b""
elif ( # now collect the args,
hasCommandBeenCollected
and hasArgLenBeenCollected
Expand All @@ -201,50 +188,54 @@ def _onReceive(self, data: bytes):

def _processCommand(self, command: bytes, arg: bytes) -> None:
if command == SEIKA_INFO:
self._handInfo(arg)
self._handleInfo(arg)
elif command == SEIKA_ROUTING:
self._handRouting(arg)
self._handleRouting(arg)
elif command == SEIKA_KEYS:
self._handKeys(arg)
self._handleKeys(arg)
elif command == SEIKA_KEYS_ROU:
self._handKeysRouting(arg)
self._handleKeysRouting(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)
def _handleInfo(self, arg: bytes):
"""After sending a request for information from the braille device this data is returned to complete
the handshake.
"""
self.numBtns = arg[0]
self.numCells = arg[1]
self.numRoutingKeys = arg[2]
try:
self._description = arg[3:].decode("ascii")
except UnicodeDecodeError:
log.debugWarning(f"Unable to decode Seika Notetaker description {arg[3:]}")

def _handleRouting(self, arg: bytes):
routingIndexes = _getRoutingIndexes(arg)
for routingIndex in routingIndexes:
gesture = InputGestureRouting(routingIndex)
try:
inputCore.manager.executeGesture(gesture)
except inputCore.NoInputGestureAction:
log.debug("No action for Seika Notetaker routing command")

def _handleKeys(self, arg: bytes):
brailleDots = arg[0]
key = arg[1] | (arg[2] << 8)
gestures = []
if key:
gestures.append(InputGesture(keys=key))
if brailleDots:
gesture = InputGesture(dots=brailleDots)
if gesture is not None:
gestures.append(InputGesture(dots=brailleDots))
for gesture in gestures:
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)
def _handleKeysRouting(self, arg: bytes):
self._handleRouting(arg[3:])
self._handleKeys(arg[:3])

gestureMap = inputCore.GlobalGestureMap({
"globalCommands.GlobalCommands": {
Expand Down Expand Up @@ -290,6 +281,22 @@ def __init__(self, index):
self.routingIndex = index


def _getKeyNames(keys: int, names: Dict[int, str]) -> Set[str]:
"""Converts a bitset of hardware buttons and keys to their equivalent names"""
return {keyName for bitFlag, keyName in names.items() if bitFlag & keys}


def _getRoutingIndexes(routingKeyBytes: bytes) -> Set[int]:
"""Converts a bitset of routing keys to their 0-index, up to 15 or 39 depending on the device"""
bitsPerByte = 8
# Convert bytes into a single bitset int
combinedRoutingKeysBitSet = sum(
[value << (bitsPerByte * bitIndex) for bitIndex, value in enumerate(routingKeyBytes)]
)
numRoutingKeys = len(routingKeyBytes) * bitsPerByte
return {i for i in range(numRoutingKeys) if (1 << i) & combinedRoutingKeysBitSet}


class InputGesture(braille.BrailleDisplayGesture, brailleInput.BrailleInputGesture):
source = BrailleDisplayDriver.name

Expand All @@ -298,13 +305,13 @@ def __init__(self, keys=None, dots=None, space=False, routing=None):
# 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)
names.update(_getKeyNames(keys, _keyNames))
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)
names.update(_getKeyNames(dots, _dotNames))
elif routing is not None:
self.routingIndex = routing
names.add('routing')
Expand Down
103 changes: 73 additions & 30 deletions tests/unit/test_brailleDisplayDrivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,90 @@
"""Unit tests for braille display drivers.
"""

from brailleDisplayDrivers.seikantk import BrailleDisplayDriver as SeikaNotetakerDriver, SEIKA_INFO

from typing import Set
from brailleDisplayDrivers import seikantk
import unittest
import braille


class TestSeikaNotetakerDriver(unittest.TestCase):
def test_onReceive(self):
""" Tests how the Seika Notetaker driver handles receiving data via `_onReceive`.
Simulates sending a sample message from the device, which should result in our driver processing a
command via `_processCommand`. Without knowing the specifications of the device, this simulation may
be inaccurate or uncomprehensive.
class FakeseikantkDriver(seikantk.BrailleDisplayDriver):
def __init__(self):
"""Sets the variables necessary to test _onReceive without a braille device connected.
"""
sampleCommand = SEIKA_INFO
sampleArgument = b"test"
sampleArgLen = bytes([len(sampleArgument)])
sampleMessage = sampleCommand + sampleArgLen + sampleArgument + b"\0\0\0"
# Variables that need to be set to spoof receiving data
self._hidBuffer = b""
self._command = None
self._argsLen = None
# Used to capture information for testing
self._pressedKeys = set()
self._routingIndexes = set()

def _handleKeys(self, arg: bytes):
"""Overridden method to capture data"""
brailleDots = arg[0]
keys = arg[1] | (arg[2] << 8)
self._pressedKeys = set(seikantk._getKeyNames(keys, seikantk._keyNames)).union(
seikantk._getKeyNames(brailleDots, seikantk._dotNames)
)

def _handleRouting(self, arg: bytes):
"""Overridden method to capture data"""
self._routingIndexes = seikantk._getRoutingIndexes(arg)

def simulateMessageReceived(self, sampleMessage: bytes):
PRE_CANARY = bytes([2]) # start of text character
POST_CANARY = bytes([3]) # end of text character

class FakeSeikaNotetakerDriver(SeikaNotetakerDriver):
def __init__(self):
"""Sets the variables necessary to test _onReceive without a braille device connected.
"""
self._hidBuffer = b""
self._command = None
self._argsLen = None

def _processCommand(self, command, arg):
"""Intercept processCommand to confirm _onReceive processes a message correctly.
"""
self._finalCommand = command
self._finalArg = arg

seikaTestDriver = FakeSeikaNotetakerDriver()
for byteToSend in sampleMessage:
# the middle byte is the only one used, padded by a byte on either side.
seikaTestDriver._onReceive(PRE_CANARY + bytes([byteToSend]) + POST_CANARY)
self._onReceive(PRE_CANARY + bytes([byteToSend]) + POST_CANARY)


class TestseikantkDriver(unittest.TestCase):
def test_handleInfo(self):
SBDDesc = b"foobarloremips" # a dummy description as this isn't specified in the spec
example16Cell = bytes([0xff, 0xff, 0xa2, 0x11, 0x16, 0x10, 0x10]) + SBDDesc
example40Cell = bytes([0xff, 0xff, 0xa2, 0x11, 0x16, 0x28, 0x28]) + SBDDesc
seikaTestDriver = FakeseikantkDriver()
seikaTestDriver.simulateMessageReceived(example16Cell)
self.assertEqual(22, seikaTestDriver.numBtns)
self.assertEqual(16, seikaTestDriver.numCells)
self.assertEqual(16, seikaTestDriver.numRoutingKeys)
self.assertEqual(SBDDesc.decode("UTF-8"), seikaTestDriver._description)

seikaTestDriver = FakeseikantkDriver()
seikaTestDriver.simulateMessageReceived(example40Cell)
self.assertEqual(22, seikaTestDriver.numBtns)
self.assertEqual(40, seikaTestDriver.numCells)
self.assertEqual(40, seikaTestDriver.numRoutingKeys)
self.assertEqual(SBDDesc.decode("UTF-8"), seikaTestDriver._description)

def test_handleRouting(self):
example16Cell = bytes([0xff, 0xff, 0xa4, 0x02, 0b10000001, 0b10000001])
example40Cell = bytes([0xff, 0xff, 0xa4, 0x05, 0b10000001, 0b10000001, 0b10000001, 0b10000001, 0b10000001])
self._simulateKeyPress(example16Cell, set(), {0, 7, 8, 15})
self._simulateKeyPress(example40Cell, set(), {0, 7, 8, 15, 16, 23, 24, 31, 32, 39})

def test_handleKeys(self):
example4 = bytes([0xff, 0xff, 0xa6, 0x03, 0b10000001, 0x00, 0b00100000])
self._simulateKeyPress(example4, {"d1", "d8", "RJ_DOWN"}, set())

def test_handleKeysAndRouting(self):
example16Cell = bytes([0xff, 0xff, 0xa8, 0x05, 0x00, 0b10010000, 0x00, 0x00, 0x40])
example40Cell = bytes([0xff, 0xff, 0xa8, 0x08, 0x00, 0b00100000, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00])
self._simulateKeyPress(example16Cell, {"LJ_CENTER", "LJ_UP"}, {14})
self._simulateKeyPress(example40Cell, {"LJ_LEFT", "LJ_DOWN"}, {17})

self.assertEqual(sampleCommand, seikaTestDriver._finalCommand)
self.assertEqual(sampleArgLen + sampleArgument, seikaTestDriver._finalArg)
def _simulateKeyPress(
self,
sampleMessage: bytes,
expectedKeyNames: Set[str],
expectedRoutingIndexes: Set[int]
):
seikaTestDriver = FakeseikantkDriver()
seikaTestDriver.simulateMessageReceived(sampleMessage)
self.assertEqual(expectedKeyNames, seikaTestDriver._pressedKeys)
self.assertEqual(expectedRoutingIndexes, seikaTestDriver._routingIndexes)


class TestGestureMap(unittest.TestCase):
Expand Down
3 changes: 2 additions & 1 deletion user_docs/en/changes.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ What's New in NVDA
- New braille tables: Bulgarian grade 1, Burmese grade 1, Burmese grade 2, Kazakh grade 1, Khmer grade 1, Northern Kurdish grade 0, Sepedi grade 1, Sepedi grade 2, Sesotho grade 1, Sesotho grade 2, Setswana grade 1, Setswana grade 2, Tatar grade 1, Vietnamese grade 0, Vietnamese grade 2, Southern Vietnamese grade 1, Xhosa grade 1, Xhosa grade 2, Yakut grade 1, Zulu grade 1, Zulu grade 2
-
- The braille input works properly with the following contracted tables: Arabic grade 2, Spanish grade 2, Urdu grade 2, Chinese (China, Mandarin) grade 2. (#12541)
-
- The COM Registration Fixing Tool now resolves more issues, especially on 64 bit Windows. (#1256)
- Improvements to button handling for the Seika Notetaker braille device from Nippon Telesoft. (#12598)
-


== Changes for Developers ==
Expand Down