Skip to content

Commit 2eae9a1

Browse files
authored
Merge 45f8bb7 into dd44cbb
2 parents dd44cbb + 45f8bb7 commit 2eae9a1

2 files changed

Lines changed: 129 additions & 20 deletions

File tree

source/brailleDisplayDrivers/hims.py

Lines changed: 122 additions & 19 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
@@ -236,14 +281,16 @@ def __init__(self, port="auto"):
236281

237282
for match in self._getTryPorts(port):
238283
portType, portId, port, portInfo = match
239-
self.isBulk = portType == bdDetect.DeviceType.CUSTOM
240284
# Try talking to the display.
241285
try:
242-
if self.isBulk:
243-
# onReceiveSize based on max packet size according to USB endpoint information.
244-
self._dev = hwIo.Bulk(port, 0, 1, self._onReceive, onReceiveSize=64)
245-
else:
246-
self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, parity=PARITY, timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._onReceive)
286+
match portType:
287+
case bdDetect.DeviceType.HID:
288+
self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive)
289+
case bdDetect.DeviceType.CUSTOM:
290+
# onReceiveSize based on max packet size according to USB endpoint information.
291+
self._dev = hwIo.Bulk(port, 0, 1, self._onReceive, onReceiveSize=64)
292+
case _:
293+
self._dev = hwIo.Serial(port, baudrate=BAUD_RATE, parity=PARITY, timeout=self.timeout, writeTimeout=self.timeout, onReceive=self._onReceive)
247294
except EnvironmentError:
248295
log.debugWarning("", exc_info=True)
249296
continue
@@ -278,11 +325,26 @@ def __init__(self, port="auto"):
278325
def display(self, cells: List[int]):
279326
# cells will already be padded up to numCells.
280327
cellBytes = bytes(cells)
281-
self._sendPacket(b"\xfc", b"\x01", cellBytes)
328+
if self.isHID:
329+
outputReport: bytes = b"".join([
330+
intToByte(self.numCells), # length
331+
cellBytes
332+
])
333+
334+
self._dev.setOutputReport(outputReport)
335+
else:
336+
self._sendPacket(b"\xfc", b"\x01", cellBytes)
282337

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

287349
def _sendIdentificationRequests(self, match: bdDetect.DeviceMatch):
288350
log.debug("Considering sending identification requests for device %s"%str(match))
@@ -293,7 +355,7 @@ def _sendIdentificationRequests(self, match: bdDetect.DeviceMatch):
293355
and match.id.startswith(modelTuple[1].bluetoothPrefix)
294356
)
295357
]
296-
else: # USB Bulk, Serial
358+
else: # USB Bulk, Serial, HID
297359
matchedModelsMap = [
298360
modelTuple for modelTuple in modelMap if(
299361
modelTuple[1].usbId == match.id
@@ -385,6 +447,36 @@ def _handlePacket(self, packet: bytes):
385447
elif mode == 0x02: # Cell count
386448
self.numCells = packet[3]
387449

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

702794
source = BrailleDisplayDriver.name
703795

704-
def __init__(self, model, keys):
796+
def __init__(self, model, keys, isHid=False):
705797
super(KeyInputGesture, self).__init__()
706798
# Model identifiers should not contain spaces.
707799
self.model=model.name.replace(" ", "")
@@ -710,15 +802,26 @@ def __init__(self, model, keys):
710802
isBrailleInput = True
711803
for key in keys:
712804
if isBrailleInput:
713-
if 0xff & key:
714-
self.dots |= key
715-
elif model.keys.get(key)=="space":
716-
self.space = True
805+
if isHid:
806+
if 8 <= int(math.log(key, 2)) <= 15:
807+
self.dots |= key >> 8
808+
elif model.keys.get(key) == "space":
809+
self.space = True
810+
else:
811+
# This is not braille input.
812+
isBrailleInput = False
813+
self.dots = 0
814+
self.space = False
717815
else:
718-
# This is not braille input.
719-
isBrailleInput = False
720-
self.dots = 0
721-
self.space = False
816+
if 0xff & key:
817+
self.dots |= key
818+
elif model.keys.get(key) == "space":
819+
self.space = True
820+
else:
821+
# This is not braille input.
822+
isBrailleInput = False
823+
self.dots = 0
824+
self.space = False
722825
names.add(model.keys[key])
723826

724827
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)