Skip to content

Commit 1bc7e5c

Browse files
authored
Merge 2706c96 into 5657960
2 parents 5657960 + 2706c96 commit 1bc7e5c

3 files changed

Lines changed: 398 additions & 0 deletions

File tree

source/bdDetect.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,3 +629,8 @@ def driverSupportsAutoDetection(driver):
629629
addUsbDevices("superBrl", KEY_SERIAL, {
630630
"VID_10C4&PID_EA60", # SuperBraille 3.2
631631
})
632+
633+
# albatross
634+
addUsbDevices("albatross", KEY_SERIAL, {
635+
"VID_0403&PID_6001", # Caiku Albatross 46/80
636+
})
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# This file is covered by the GNU General Public License.
3+
# See the file COPYING for more details.
4+
# Copyright (C) 2021 NV Access Limited, Burman's Computer and Education Ltd.
5+
6+
from typing import List
7+
from threading import Timer
8+
import serial
9+
import braille
10+
import hwIo
11+
from hwIo import intToByte
12+
import time
13+
import inputCore
14+
from logHandler import log
15+
16+
BAUD_RATE = 19200
17+
TIMEOUT = 0.2
18+
WRITE_TIMEOUT = 0
19+
# Indexes are key codes sent by display.
20+
KEY_NAMES = {
21+
1: "attribute1",
22+
42: "attribute2",
23+
83: "f1",
24+
84: "f2",
25+
85: "f3",
26+
86: "f4",
27+
87: "f5",
28+
88: "f6",
29+
89: "f7",
30+
90: "f8",
31+
91: "home1",
32+
92: "end1",
33+
93: "eCursor1",
34+
94: "cursor1",
35+
95: "up1",
36+
96: "down1",
37+
97: "left",
38+
98: "up2",
39+
103: "lWheelRight",
40+
104: "lWheelLeft",
41+
105: "lWheelUp",
42+
106: "lWheelDown",
43+
151: "attribute3",
44+
192: "attribute4",
45+
193: "f9",
46+
194: "f10",
47+
195: "f11",
48+
196: "f12",
49+
197: "f13",
50+
198: "f14",
51+
199: "f15",
52+
200: "f16",
53+
201: "home2",
54+
202: "end2",
55+
203: "eCursor2",
56+
204: "cursor2",
57+
205: "up3",
58+
206: "down2",
59+
207: "right",
60+
208: "down3",
61+
213: "rWheelRight",
62+
214: "rWheelLeft",
63+
215: "rWheelUp",
64+
216: "rWheelDown",
65+
}
66+
# These are ctrl-keys which may start key combination.
67+
CONTROL_KEY_CODES: List[int] = [
68+
1, 42, 83, 84, 89, 90, 91, 92, 93, 94, 151, 192, 193, 194, 199, 200, 201, 202, 203, 204, ]
69+
# Send this to Albatross to confirm that connection is established.
70+
ESTABLISHED = b"\xfe\xfd\xfe\xfd"
71+
# Send information to Albatross enclosed by these bytes.
72+
START_BYTE = b"\xfb"
73+
END_BYTE = b"\xfc"
74+
# To keep connected these both bytes must be sent periodically.
75+
BOTH_BYTES = b"\xfb\xfc"
76+
77+
78+
# Timer is used for that purpose and to reconnect with display (copied from
79+
# https://stackoverflow.com/questions/474528/what-is-the-best-way-to-repeatedly-execute-a-function-every-x-seconds)
80+
class RepeatedTimer(object):
81+
def __init__(self, interval, function, *args, **kwargs):
82+
self._timer = None
83+
self.interval = interval
84+
self.function = function
85+
self.args = args
86+
self.kwargs = kwargs
87+
self.is_running = False
88+
self.next_call = time.time()
89+
self.start()
90+
91+
def _run(self):
92+
self.is_running = False
93+
self.start()
94+
self.function(*self.args, **self.kwargs)
95+
96+
def start(self):
97+
if not self.is_running:
98+
self.next_call += self.interval
99+
self._timer = Timer(self.next_call - time.time(), self._run)
100+
self._timer.start()
101+
self.is_running = True
102+
103+
def stop(self):
104+
self._timer.cancel()
105+
self.is_running = False
106+
107+
108+
class BrailleDisplayDriver(braille.BrailleDisplayDriver):
109+
name = "albatross"
110+
# Translators: Names of braille displays.
111+
description = _("Caiku Albatross 46/80")
112+
isThreadSafe = True
113+
114+
@classmethod
115+
def getManualPorts(cls):
116+
return braille.getSerialPorts()
117+
118+
def chkPort(self, port: bytes) -> bool:
119+
try:
120+
self._dev = hwIo.Serial(
121+
port, baudrate=BAUD_RATE, stopbits=serial.STOPBITS_ONE, parity=serial.PARITY_NONE,
122+
timeout=TIMEOUT, writeTimeout=WRITE_TIMEOUT, onReceive=self._onReceive)
123+
except EnvironmentError:
124+
log.debugWarning("", exc_info=True)
125+
return False
126+
return True
127+
128+
# Whole display should be updated at next time.
129+
def clearOldCells(self):
130+
i = 0
131+
for cell in self.oldCells:
132+
self.oldCells[i] = 0
133+
i += 1
134+
135+
# All write operations are done here.
136+
def sendToDisplay(self, data: bytes) -> bool:
137+
try:
138+
self._dev.write(data)
139+
except serial.serialutil.SerialException:
140+
# Suitable initial values for reconnection.
141+
# Connection worked before failure.
142+
if self.numCells:
143+
self.numCells = 0
144+
self.clearOldCells()
145+
self._dev.close()
146+
self._dev = None
147+
if not self.chkPort(self.currentPort):
148+
# Maybe display was unplugged/powered off which causes USB serial port disappearing.
149+
self.tryReconnect = True
150+
if self._dev:
151+
self._dev.close()
152+
return False
153+
if data == ESTABLISHED:
154+
# Actually I/O buffer should be reseted, but poor man's solution now.
155+
while len(self._dev.read(1024)):
156+
pass
157+
return True
158+
159+
def keepConnected(self):
160+
# Can disappeared port be found again?
161+
if self.tryReconnect:
162+
if not self.chkPort(self.currentPort):
163+
return
164+
else:
165+
# Port found.
166+
self.tryReconnect = False
167+
return
168+
if self.timerRunning and self.numCells:
169+
self.sendToDisplay(BOTH_BYTES)
170+
171+
def __init__(self, port="Auto"):
172+
super().__init__()
173+
# Number of cells is received when initializing connection.
174+
self.numCells = 0
175+
# Keep old display data.
176+
self.oldCells: List[int] = []
177+
# Try to reconnect if needed.
178+
self.tryReconnect = False
179+
# Current portto reconnect.
180+
self.currentPort = ""
181+
self.timerRunning = False
182+
# Search ports where display can be connected.
183+
for portType, portId, port, portInfo in self._getTryPorts(port):
184+
if not self.chkPort(port):
185+
continue
186+
# Albatross seems to need some more time.
187+
while not self._dev.waitForRead(TIMEOUT):
188+
pass
189+
# Check for cell information
190+
if self.numCells:
191+
while len(self.oldCells) < self.numCells:
192+
self.oldCells.append(0)
193+
# We may need current connection port to reconnect.
194+
self.currentPort = port
195+
# Start timer to keep connection.
196+
self.rt = RepeatedTimer(1, self.keepConnected)
197+
self.timerRunning = True
198+
log.info(
199+
"Connected to Caiku Albatross %s on %s port %s at %s bps.",
200+
self.numCells, portType, port, BAUD_RATE)
201+
break
202+
# This device initialization failed.
203+
self._dev.close()
204+
else:
205+
raise RuntimeError("No Albatross found")
206+
207+
def terminate(self):
208+
try:
209+
super().terminate()
210+
if self.timerRunning:
211+
self.rt.stop()
212+
self.rt = None
213+
self.timerRunning = False
214+
# Possibly already closed.
215+
if self._dev:
216+
self._dev.close()
217+
finally:
218+
self._dev = None
219+
self.numCells = 0
220+
221+
def _onReceive(self, data: bytes):
222+
if not self.numCells:
223+
# If no connection, Albatross sends continuously byte \xff
224+
# followed by byte containing various settings like number of cells.
225+
if data != b"\xff":
226+
# Read another byte, if the first one was value byte.
227+
data = self._dev.read(1)
228+
if len(data) == 0 or data != b"\xff":
229+
return
230+
log.debugWarning("Init byte: %r" % data)
231+
data = self._dev.read(1)
232+
if len(data) == 0:
233+
return
234+
log.debugWarning("Value byte: %r" % data)
235+
if not self.sendToDisplay(ESTABLISHED):
236+
return
237+
self.numCells = 80 if ord(data) >> 7 == 1 else 46
238+
return
239+
# Connected.
240+
else:
241+
# It is possible that there is no connection from perspective of display.
242+
if data == b"\xff":
243+
if self.sendToDisplay(ESTABLISHED):
244+
self.clearOldCells()
245+
log.debugWarning("Byte %r, numCells %d, tryReconnect %r" % (data, self.numCells, self.tryReconnect))
246+
return
247+
pressedKeys = set()
248+
# If Ctrl-key is pressed, then there is at least one byte to read;
249+
# in single ctrl-key presses and key combinations the first key is resent as last one.
250+
if ord(data) in CONTROL_KEY_CODES:
251+
# at most 4 keys.
252+
kPresses = bytearray(data + self._dev.read(4))
253+
[pressedKeys.add(k) for k in kPresses]
254+
else:
255+
pressedKeys.add(ord(data))
256+
try:
257+
inputCore.manager.executeGesture(InputGestureKeys(pressedKeys))
258+
except inputCore.NoInputGestureAction:
259+
pass
260+
261+
def display(self, cells: List[int]):
262+
if not self.numCells:
263+
return
264+
writeBytes: List[bytes] = [START_BYTE, ]
265+
# Only changed content is sent, variable i is used to show position on display.
266+
i = 1
267+
j = 0
268+
for cell in cells:
269+
if cell != self.oldCells[j]:
270+
self.oldCells[j] = cell
271+
writeBytes.append(intToByte(i))
272+
# Bits have to be reversed.
273+
writeBytes.append(intToByte(int('{:08b}'.format(cell)[::-1], 2)))
274+
i += 1
275+
j += 1
276+
writeBytes.append(END_BYTE)
277+
self.sendToDisplay(b"".join(writeBytes))
278+
279+
gestureMap = inputCore.GlobalGestureMap({
280+
"globalCommands.GlobalCommands": {
281+
"braille_scrollBack": ("br(albatross):left",),
282+
"braille_scrollForward": ("br(albatross):right",),
283+
"braille_previousLine": ("br(albatross):up1", "br(albatross):up2", "br(albatross):up3",),
284+
"braille_nextLine": ("br(albatross):down1", "br(albatross):down2", "br(albatross):down3",),
285+
"braille_routeTo": ("br(albatross):routing",),
286+
"braille_reportFormatting": ("br(albatross):secondRouting",),
287+
"braille_toggleTether": ("br(albatross):eCursor1", "br(albatross):eCursor2",),
288+
"braille_toFocus": ("br(albatross):cursor1", "br(albatross):cursor2",),
289+
"review_top": ("br(albatross):home1", "br(albatross):home2",),
290+
"review_bottom": ("br(albatross):end1", "br(albatross):end2",),
291+
"braille_toggleFocusContextPresentation": ("br(albatross):eCursor1+eCursor2",),
292+
"reviewMode_previous": ("br(albatross):f1",),
293+
"reviewMode_next": ("br(albatross):f2",),
294+
"navigatorObject_parent": ("br(albatross):f3",),
295+
"navigatorObject_firstChild": ("br(albatross):f4",),
296+
"navigatorObject_previous": ("br(albatross):f5",),
297+
"navigatorObject_next": ("br(albatross):f6",),
298+
"navigatorObject_moveFocus": ("br(albatross):f7",),
299+
"review_activate": ("br(albatross):f8",),
300+
"navigatorObject_toFocus": ("br(albatross):f1+f2",),
301+
"navigatorObject_current": ("br(albatross):f7+f8",),
302+
"dateTime": ("br(albatross):f9",),
303+
"showGui": ("br(albatross):f10",),
304+
"title": ("br(albatross):f11",),
305+
"reportStatusLine": ("br(albatross):f12",),
306+
"reportCurrentLine": ("br(albatross):f13",),
307+
"review_currentCharacter": ("br(albatross):f14",),
308+
"sayAll": ("br(albatross):f15",),
309+
"speechMode": ("br(albatross):f16",),
310+
"kb:windows+d": ("br(albatross):attribute1"),
311+
"kb:windows+e": ("br(albatross):attribute2"),
312+
"kb:windows+b": ("br(albatross):attribute3"),
313+
"kb:windows+i": ("br(albatross):attribute4"),
314+
},
315+
})
316+
317+
318+
class InputGestureKeys(braille.BrailleDisplayGesture):
319+
320+
source = BrailleDisplayDriver.name
321+
322+
def __init__(self, keys):
323+
super().__init__()
324+
self.keyCodes = set(keys)
325+
326+
self.keyNames = names = []
327+
for key in self.keyCodes:
328+
if 2 <= key <= 41 or 111 <= key <= 150:
329+
names.append("routing")
330+
if 2 <= key <= 41:
331+
self.routingIndex = key - 2
332+
if 111 <= key <= 150:
333+
self.routingIndex = key - 71
334+
elif 43 <= key <= 82 or 152 <= key <= 191:
335+
names.append("secondRouting")
336+
if 43 <= key <= 82:
337+
self.routingIndex = key - 43
338+
if 152 <= key <= 191:
339+
self.routingIndex = key - 112
340+
else:
341+
try:
342+
names.append(KEY_NAMES[key])
343+
except KeyError:
344+
log.debugWarning("Unknown key with id %d" % key)
345+
346+
self.id = "+".join(names)

0 commit comments

Comments
 (0)