Skip to content

Commit 3377ac0

Browse files
authored
Merge 0ae8a6c into dd44cbb
2 parents dd44cbb + 0ae8a6c commit 3377ac0

2 files changed

Lines changed: 125 additions & 15 deletions

File tree

source/brailleDisplayDrivers/hims.py

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import serial
1010
from io import BytesIO
1111
import hwIo
12+
from hwIo import intToByte
1213
import braille
1314
from logHandler import log
1415
from collections import OrderedDict
@@ -17,10 +18,14 @@
1718
from baseObject import AutoPropertyObject
1819
import time
1920
import bdDetect
21+
import math
2022

2123
BAUD_RATE = 115200
2224
PARITY = serial.PARITY_NONE
2325

26+
# HID
27+
HR_CAPS = b"\x01"
28+
2429
class Model(AutoPropertyObject):
2530
"""Extend from this base class to define model specific behavior."""
2631
#: Two bytes device identifier, used in the protocol to identify the device
@@ -135,6 +140,40 @@ def _get_keys(self) -> dict[str, str]:
135140
return keys
136141

137142

143+
class BrailleEdge3S(BrailleEdge):
144+
"""This device is the BrailleEdge which doesn't use the hims driver.
145+
It only uses a Braille HID connection.
146+
"""
147+
usbId = "VID_045E&PID_940A"
148+
149+
def _get_keys(self) -> dict[str, str]:
150+
return OrderedDict({
151+
# Braille keyboard, not used for SyncBraille
152+
0x01 << 8: "dot1",
153+
0x02 << 8: "dot2",
154+
0x04 << 8: "dot3",
155+
0x08 << 8: "dot4",
156+
0x10 << 8: "dot5",
157+
0x20 << 8: "dot6",
158+
0x40 << 8: "dot7",
159+
0x80 << 8: "dot8",
160+
0x01 << 88: "space",
161+
0x01 << 64: "f1",
162+
0x04 << 64: "f2",
163+
0x10 << 64: "f3",
164+
0x40 << 64: "f4",
165+
0x08 << 64: "leftSideScrollUp",
166+
0x08 << 64: "rightSideScrollUp",
167+
0x80 << 64: "leftSideScrollDown",
168+
0x80 << 64: "rightSideScrollDown",
169+
0x02 << 64: "leftCursor",
170+
0x20 << 64: "rightCursor",
171+
0x01 << 72: "control",
172+
0x02 << 72: "alt",
173+
0x04 << 72: "home",
174+
})
175+
176+
138177
class BrailleSense2S(BrailleSense):
139178
"""Braille Sense with one scroll key on both sides.
140179
Also referred to as Braille Sense Classic."""
@@ -191,6 +230,7 @@ def _get_keys(self):
191230
BrailleSenseQ,
192231
BrailleEdge,
193232
BrailleEdge2S,
233+
BrailleEdge3S,
194234
SmartBeetle,
195235
BrailleSense4S,
196236
BrailleSense2S,
@@ -207,6 +247,11 @@ class BrailleDisplayDriver(braille.BrailleDisplayDriver):
207247

208248
@classmethod
209249
def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
250+
# Hid device
251+
driverRegistrar.addUsbDevices(bdDetect.DeviceType.HID, {
252+
"VID_045E&PID_940A", # Braille Edge3S 40
253+
})
254+
210255
# Bulk devices
211256
driverRegistrar.addUsbDevices(bdDetect.DeviceType.CUSTOM, {
212257
"VID_045E&PID_930A", # Braille Sense & Smart Beetle
@@ -237,9 +282,12 @@ def __init__(self, port="auto"):
237282
for match in self._getTryPorts(port):
238283
portType, portId, port, portInfo = match
239284
self.isBulk = portType == bdDetect.DeviceType.CUSTOM
285+
self.isHID = portType == bdDetect.DeviceType.HID
240286
# Try talking to the display.
241287
try:
242-
if self.isBulk:
288+
if self.isHID:
289+
self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive)
290+
elif self.isBulk:
243291
# onReceiveSize based on max packet size according to USB endpoint information.
244292
self._dev = hwIo.Bulk(port, 0, 1, self._onReceive, onReceiveSize=64)
245293
else:
@@ -278,11 +326,26 @@ def __init__(self, port="auto"):
278326
def display(self, cells: List[int]):
279327
# cells will already be padded up to numCells.
280328
cellBytes = bytes(cells)
281-
self._sendPacket(b"\xfc", b"\x01", cellBytes)
329+
if self.isHID:
330+
outputReport: bytes = b"".join([
331+
intToByte(self.numCells), # length
332+
cellBytes
333+
])
334+
335+
self._dev.setOutputReport(outputReport)
336+
else:
337+
self._sendPacket(b"\xfc", b"\x01", cellBytes)
282338

283339
def _sendCellCountRequest(self):
284-
log.debug("Sending cell count request...")
285-
self._sendPacket(b"\xfb", b"\x01", bytes(32)) # send 32 null bytes
340+
if self.isHID:
341+
try:
342+
data: bytes = self._dev.getFeature(HR_CAPS)
343+
self.numCells = data[9]
344+
except WindowsError:
345+
return # Fail!
346+
else:
347+
log.debug("Sending cell count request...")
348+
self._sendPacket(b"\xfb", b"\x01", bytes(32)) # send 32 null bytes
286349

287350
def _sendIdentificationRequests(self, match: bdDetect.DeviceMatch):
288351
log.debug("Considering sending identification requests for device %s"%str(match))
@@ -293,7 +356,7 @@ def _sendIdentificationRequests(self, match: bdDetect.DeviceMatch):
293356
and match.id.startswith(modelTuple[1].bluetoothPrefix)
294357
)
295358
]
296-
else: # USB Bulk, Serial
359+
else: # USB Bulk, Serial, HID
297360
matchedModelsMap = [
298361
modelTuple for modelTuple in modelMap if(
299362
modelTuple[1].usbId == match.id
@@ -385,6 +448,36 @@ def _handlePacket(self, packet: bytes):
385448
elif mode == 0x02: # Cell count
386449
self.numCells = packet[3]
387450

451+
def _hidOnReceive(self, data: bytes):
452+
if data == bytes([0x00] * 12): # All key lifted
453+
return
454+
455+
routingKey = int.from_bytes(data[2:7], "little", signed=False)
456+
if routingKey != 0: # Routing key
457+
try:
458+
inputCore.manager.executeGesture(RoutingInputGesture(int(math.log(routingKey, 2))))
459+
except inputCore.NoInputGestureAction:
460+
pass
461+
else: # Other key
462+
if not self._model:
463+
return
464+
_keys = int.from_bytes(data, "little", signed=False)
465+
keys = set()
466+
for keyHex in self._model.keys:
467+
if _keys & keyHex:
468+
# This key is pressed
469+
_keys -= keyHex
470+
keys.add(keyHex)
471+
if _keys == 0:
472+
break
473+
if _keys:
474+
log.error("Unknown key(s) 0x%x received from Hims display" % _keys)
475+
return
476+
try:
477+
inputCore.manager.executeGesture(KeyInputGesture(self._model, keys, True))
478+
except inputCore.NoInputGestureAction:
479+
pass
480+
388481
def _onReceive(self, data: bytes):
389482
if self.isBulk:
390483
# data contains the entire packet.
@@ -701,7 +794,7 @@ class KeyInputGesture(braille.BrailleDisplayGesture, brailleInput.BrailleInputGe
701794

702795
source = BrailleDisplayDriver.name
703796

704-
def __init__(self, model, keys):
797+
def __init__(self, model, keys, isHid=False):
705798
super(KeyInputGesture, self).__init__()
706799
# Model identifiers should not contain spaces.
707800
self.model=model.name.replace(" ", "")
@@ -710,15 +803,26 @@ def __init__(self, model, keys):
710803
isBrailleInput = True
711804
for key in keys:
712805
if isBrailleInput:
713-
if 0xff & key:
714-
self.dots |= key
715-
elif model.keys.get(key)=="space":
716-
self.space = True
806+
if isHid:
807+
if 8 <= int(math.log(key, 2)) <= 15:
808+
self.dots |= key >> 8
809+
elif model.keys.get(key) == "space":
810+
self.space = True
811+
else:
812+
# This is not braille input.
813+
isBrailleInput = False
814+
self.dots = 0
815+
self.space = False
717816
else:
718-
# This is not braille input.
719-
isBrailleInput = False
720-
self.dots = 0
721-
self.space = False
817+
if 0xff & key:
818+
self.dots |= key
819+
elif model.keys.get(key) == "space":
820+
self.space = True
821+
else:
822+
# This is not braille input.
823+
isBrailleInput = False
824+
self.dots = 0
825+
self.space = False
722826
names.add(model.keys[key])
723827

724828
self.id = "+".join(names)

user_docs/en/changes.t2t

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ What's New in NVDA
33

44
%!includeconf: ../changes.t2tconf
55

6+
= 2024.3 =
7+
8+
== New Features ==
9+
- Added support for the BrailleEdgeS3 braille device(#16279, @EdKweon).
10+
11+
612
= 2024.2 =
713

814
== New Features ==
@@ -30,7 +36,7 @@ What's New in NVDA
3036
-
3137
- Added a new unassigned input gesture to toggle the reporting of figures and captions. (#10826, #14349)
3238
-
33-
- Added support for the BrailleEdgeS2 braille device. (#16033)
39+
- Added support for the BrailleEdgeS2 braille device. (#16033, @EdKweon)
3440
- NVDA will keep the audio device awake after speech stops, in order to prevent the start of the next speech being clipped with some audio devices such as Bluetooth headphones. (#14386, @jcsteh, @mltony)
3541
- Sound split: (#12985, @mltony)
3642
- Allows splitting NVDA sounds in one channel (e.g. left) while sounds from all other applications in the other channel (e.g. right).

0 commit comments

Comments
 (0)