Skip to content

Commit 82c1e23

Browse files
authored
Merge 0627c24 into b5f82f8
2 parents b5f82f8 + 0627c24 commit 82c1e23

4 files changed

Lines changed: 90 additions & 38 deletions

File tree

source/bdDetect.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,3 +706,8 @@ def driverSupportsAutoDetection(driver):
706706
addUsbDevices("seikantk", KEY_HID, {
707707
"VID_10C4&PID_EA80", # Seika Notetaker
708708
})
709+
710+
# Bluetooth name of the Seika devices is "TSM abcd", where the "abcd" is a four-digit
711+
# number, e.g. "TSM 3366", "TSM 0001", etc. There is a space between "TSM" and "abcd".
712+
seikaBluetoothNameRegex = re.compile(r"TSM \d\d\d\d")
713+
addBluetoothDevices("seikantk", lambda m: bool(seikaBluetoothNameRegex.match(m.deviceInfo["bluetoothName"])))

source/braille.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2493,7 +2493,7 @@ def getManualPorts(cls) -> typing.Iterator[typing.Tuple[str, str]]:
24932493
@classmethod
24942494
def _getTryPorts(
24952495
cls, port: Union[str, bdDetect.DeviceMatch]
2496-
) -> Iterable[bdDetect.DeviceMatch]:
2496+
) -> typing.Iterator[bdDetect.DeviceMatch]:
24972497
"""Returns the ports for this driver to which a connection attempt should be made.
24982498
This generator function is usually used in L{__init__} to connect to the desired display.
24992499
@param port: the port to connect to.

source/brailleDisplayDrivers/seika.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def __init__(self):
9292
# Read out the input buffer
9393
versionS = self._ser.read(12)
9494
log.debug(f"receive {versionS}")
95-
if versionS.startswith(prefix=(
95+
if versionS.startswith((
9696
b'\x00\x05(\x08v5.0\x01\x01\x01\x01',
9797
b'\x00\x05(\x08seika\x00'
9898
)):

source/brailleDisplayDrivers/seikantk.py

Lines changed: 83 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
# This file represents the braille display driver for
77
# Seika Notetaker, a product from Nippon Telesoft
88
# see www.seika-braille.com for more details
9-
109
from io import BytesIO
1110
import typing
1211
from typing import Dict, List, Set
1312

13+
import serial
14+
1415
import braille
1516
import brailleInput
1617
import inputCore
17-
import hwPortUtils
1818
import bdDetect
1919
import hwIo
2020
from serial.win32 import INVALID_HANDLE_VALUE
@@ -57,66 +57,104 @@
5757
SEIKA_KEYS = b"\xff\xff\xa6"
5858
SEIKA_KEYS_ROU = b"\xff\xff\xa8"
5959

60-
SEIKA_CONFIG = b"\x50\x00\x00\x25\x80\x00\x00\x03\x00"
60+
BAUD = 9600
61+
SEIKA_HID_FEATURES = b"".join([
62+
b"\x50\x00\x00",
63+
int.to_bytes(BAUD, length=2, byteorder="big", signed=False), # b"\x25\x80"
64+
b"\x00\x00\x03\x00",
65+
])
6166
SEIKA_CMD_ON = b"\x41\x01"
6267

6368
vidpid = "VID_10C4&PID_EA80"
6469
hidvidpid = "HID\\VID_10C4&PID_EA80"
6570
SEIKA_NAME = "seikantk"
6671

67-
6872
class BrailleDisplayDriver(braille.BrailleDisplayDriver):
6973
_dev: hwIo.IoBase
7074
name = SEIKA_NAME
7175
# Translators: Name of a braille display.
7276
description = _("Seika Notetaker")
73-
path = ""
7477
isThreadSafe = True
75-
for d in hwPortUtils.listHidDevices():
76-
if d["hardwareID"].startswith(hidvidpid):
77-
path = d["devicePath"]
7878

7979
@classmethod
8080
def getManualPorts(cls) -> typing.Iterator[typing.Tuple[str, str]]:
8181
"""@return: An iterator containing the name and description for each port.
8282
"""
8383
return braille.getSerialPorts()
8484

85-
def __init__(self, port="hid"):
85+
def __init__(self, port=bdDetect.KEY_HID):
8686
super().__init__()
8787
self.numCells = 0
8888
self.numBtns = 0
8989
self.numRoutingKeys = 0
9090
self.handle = None
91-
9291
self._hidBuffer = b""
9392
self._command: typing.Optional[bytes] = None
9493
self._argsLen: typing.Optional[int] = None
95-
log.info(f"Seika Notetaker braille driver path: {self.path}")
96-
97-
if self.path == "":
98-
raise RuntimeError("No MINI-SEIKA display found, no path found")
99-
self._dev = dev = hwIo.Hid(path=self.path, onReceive=self._onReceive)
100-
if dev._file == INVALID_HANDLE_VALUE:
101-
raise RuntimeError("No MINI-SEIKA display found, open error")
102-
dev.setFeature(SEIKA_CONFIG) # baud rate, stop bit usw
103-
dev.setFeature(SEIKA_CMD_ON) # device on
94+
95+
log.debug(f"Seika Notetaker braille driver: ({port!r})")
96+
dev: typing.Optional[typing.Union[hwIo.Hid, hwIo.Serial]] = None
97+
for match in self._getTryPorts(port):
98+
self.isHid = match.type == bdDetect.KEY_HID
99+
self.isSerial = match.type == bdDetect.KEY_SERIAL
100+
try:
101+
if self.isHid:
102+
log.info(f"Trying Seika notetaker on USB-HID")
103+
self._dev = dev = hwIo.Hid(
104+
path=match.port, # for a Hid match type 'port' is actually 'path'.
105+
onReceive=self._onReceiveHID
106+
)
107+
dev.setFeature(SEIKA_HID_FEATURES) # baud rate, stop bit usw
108+
dev.setFeature(SEIKA_CMD_ON) # device on
109+
elif self.isSerial:
110+
log.info(f"Trying Seika notetaker on Bluetooth (serial) port:{match.port}")
111+
self._dev = dev = hwIo.Serial(
112+
port=match.port,
113+
onReceive=self._onReceiveSerial,
114+
baudrate=BAUD,
115+
parity=serial.PARITY_NONE,
116+
bytesize=serial.EIGHTBITS,
117+
stopbits=serial.STOPBITS_ONE,
118+
)
119+
# Does the device need to be sent SEIKA_CMD_ON as per USB-HID?
120+
else:
121+
log.debug(f"Port type not handled: {match.type}")
122+
continue
123+
except EnvironmentError:
124+
log.debugWarning("", exc_info=True)
125+
continue
126+
if self._getDeviceInfo(dev):
127+
break
128+
elif dev:
129+
dev.close()
130+
dev = None
131+
132+
if not dev:
133+
RuntimeError("No MINI-SEIKA display found")
134+
elif self.numCells == 0:
135+
dev.close()
136+
dev = None
137+
raise RuntimeError("No MINI-SEIKA display found, no response")
138+
else:
139+
log.info(
140+
f"Seika notetaker,"
141+
f" Cells {self.numCells}"
142+
f" Buttons {self.numBtns}"
143+
)
144+
145+
def _getDeviceInfo(self, dev: hwIo.IoBase) -> bool:
146+
if not dev or dev._file == INVALID_HANDLE_VALUE:
147+
log.debug("No MINI-SEIKA display found, open error")
148+
return False
149+
104150
dev.write(SEIKA_REQUEST_INFO) # Request the Info from the device
105151

106152
# wait and try to get info from the Braille display
107153
for i in range(MAX_READ_ATTEMPTS): # the info-block is about
108154
dev.waitForRead(READ_TIMEOUT_SECS)
109155
if self.numCells:
110-
log.info(
111-
f"Seika notetaker on USB-HID,"
112-
f" Cells {self.numCells}"
113-
f" Buttons {self.numBtns}"
114-
)
115-
break
116-
117-
if self.numCells == 0:
118-
dev.close()
119-
raise RuntimeError("No MINI-SEIKA display found, no response")
156+
return True
157+
return False
120158

121159
def terminate(self):
122160
try:
@@ -126,17 +164,29 @@ def terminate(self):
126164

127165
def display(self, cells: List[int]):
128166
# cells will already be padded up to numCells.
129-
cellBytes = SEIKA_SEND_TEXT + self.numCells.to_bytes(1, 'little') + bytes(cells)
167+
cellBytes = SEIKA_SEND_TEXT + bytes([self.numCells]) + bytes(cells)
130168
self._dev.write(cellBytes)
131169

132-
def _onReceive(self, data: bytes):
170+
def _onReceiveHID(self, data: bytes):
171+
"""Three bytes at a time expected, only the middle byte is used to construct the command, the first
172+
and third byte are discarded.
173+
"""
174+
stream = BytesIO(data)
175+
cmd = stream.read(3) # Note, first and third bytes are discarded
176+
newByte: bytes = cmd[1:2] # use range to return bytes type, containing only index 1
177+
self._onReceive(newByte)
178+
179+
def _onReceiveSerial(self, data: bytes):
180+
"""One byte at a time is expected"""
181+
self._onReceive(data)
182+
183+
def _onReceive(self, newByte: bytes):
133184
"""
134185
Note: Further insight into this function would be greatly appreciated.
135186
This function is a very simple state machine, each stage represents the collection of a field, when all
136187
fields are collected the command they represent can be processed.
137188
138-
On each call to _onReceive three bytes are read from the device.
139-
The first and third bytes are discarded, the second byte is appended to a buffer.
189+
On each call to _onReceive the new byte is appended to a buffer.
140190
The buffer is accumulated until the buffer has the required number of bytes for the field being collected.
141191
There are 3 fields to be collected before a command can be processed:
142192
1: first 3 bytes: command
@@ -146,9 +196,6 @@ def _onReceive(self, data: bytes):
146196
After accumulating enough bytes for each phase, the buffer is cleared and the next stage is entered.
147197
"""
148198
COMMAND_LEN = 3
149-
stream = BytesIO(data)
150-
cmd = stream.read(3) # Note, first and third bytes are discarded
151-
newByte: bytes = cmd[1:2] # use range to return bytes
152199
self._hidBuffer += newByte
153200
hasCommandBeenCollected = self._command is not None
154201
hasArgLenBeenCollected = self._argsLen is not None

0 commit comments

Comments
 (0)