99import serial
1010from io import BytesIO
1111import hwIo
12+ from hwIo import intToByte
1213import braille
1314from logHandler import log
1415from collections import OrderedDict
1718from baseObject import AutoPropertyObject
1819import time
1920import bdDetect
21+ import math
2022
2123BAUD_RATE = 115200
2224PARITY = serial .PARITY_NONE
2325
26+ # HID
27+ HR_CAPS = b"\x01 "
28+
2429class 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+
138177class 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 )
0 commit comments