-
-
Notifications
You must be signed in to change notification settings - Fork 678
/
Copy pathhandyTech.py
1236 lines (1056 loc) · 35.3 KB
/
handyTech.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2008-2023 NV Access Limited, Bram Duvigneau, Babbage B.V.,
# Felix Grützmacher (Handy Tech Elektronik GmbH), Leonard de Ruijter
"""
Braille display driver for Handy Tech braille displays.
"""
from collections import OrderedDict
from typing import (
Dict,
List,
Optional,
Union,
)
from io import BytesIO
import serial
import weakref
import hwIo
from hwIo import intToByte, boolToByte
import braille
import brailleInput
import inputCore
import ui
from baseObject import ScriptableObject, AutoPropertyObject
from globalCommands import SCRCAT_BRAILLE
from logHandler import log
import bdDetect
import time
import datetime
from ctypes import windll
import windowUtils
import wx
class InvisibleDriverWindow(windowUtils.CustomWindow):
className = "Handy_Tech_Server"
HT_SLEEP = 100
HT_INCREMENT = 1
HT_DECREMENT = 0
def __init__(self):
super().__init__("Handy Tech Server")
# Register shared window message.
# Note: There is no corresponding unregister function.
# Still this does no harm if done repeatedly.
self.window_message = windll.user32.RegisterWindowMessageW("Handy_Tech_Server")
def windowProc(self, hwnd: int, msg: int, wParam: int, lParam: int):
if msg == self.window_message:
instanceCount = len(BrailleDisplayDriver._instances)
if instanceCount == 0:
log.error(
"Received Handy_Tech_Server window message while no driver instances are alive",
)
wx.CallAfter(BrailleDisplayDriver.destroyMessageWindow)
elif wParam == self.HT_SLEEP:
if instanceCount > 1:
log.error(
"Received Handy_Tech_Server window message while multiple driver instances are alive",
)
driver = next(d for d in BrailleDisplayDriver._instances)
if lParam == self.HT_INCREMENT:
driver.goToSleep()
elif lParam == self.HT_DECREMENT:
driver.wakeUp()
return 0 # success, bypass default window procedure
BAUD_RATE = 19200
PARITY = serial.PARITY_ODD
# Some older Handy Tech displays use a HID converter and an internal serial interface.
# We need to keep these IDS around here to send additional data upon connection.
USB_IDS_HID_CONVERTER = {
"VID_1FE4&PID_0003", # USB-HID adapter
"VID_1FE4&PID_0074", # Braille Star 40
"VID_1FE4&PID_0044", # Easy Braille
}
# Model identifiers
MODEL_BRAILLE_WAVE = b"\x05"
MODEL_MODULAR_EVOLUTION_64 = b"\x36"
MODEL_MODULAR_EVOLUTION_88 = b"\x38"
MODEL_MODULAR_CONNECT_88 = b"\x3a"
MODEL_EASY_BRAILLE = b"\x44"
MODEL_ACTIVE_BRAILLE = b"\x54"
MODEL_CONNECT_BRAILLE = b"\x55"
MODEL_ACTILINO = b"\x61"
MODEL_ACTIVE_STAR_40 = b"\x64"
MODEL_BASIC_BRAILLE_16 = b"\x81"
MODEL_BASIC_BRAILLE_20 = b"\x82"
MODEL_BASIC_BRAILLE_32 = b"\x83"
MODEL_BASIC_BRAILLE_40 = b"\x84"
MODEL_BASIC_BRAILLE_48 = b"\x8a"
MODEL_BASIC_BRAILLE_64 = b"\x86"
MODEL_BASIC_BRAILLE_80 = b"\x87"
MODEL_BASIC_BRAILLE_160 = b"\x8b"
MODEL_BASIC_BRAILLE_84 = b"\x8c"
MODEL_BASIC_BRAILLE_PLUS_32 = b"\x93"
MODEL_BASIC_BRAILLE_PLUS_40 = b"\x94"
MODEL_BRAILLINO = b"\x72"
MODEL_BRAILLE_STAR_40 = b"\x74"
MODEL_BRAILLE_STAR_80 = b"\x78"
MODEL_MODULAR_20 = b"\x80"
MODEL_MODULAR_80 = b"\x88"
MODEL_MODULAR_40 = b"\x89"
MODEL_ACTIVATOR = b"\xa4"
MODEL_ACTIVATOR_PRO_64 = b"\xa6"
MODEL_ACTIVATOR_PRO_80 = b"\xa8"
# Key constants
KEY_B1 = 0x03
KEY_B2 = 0x07
KEY_B3 = 0x0B
KEY_B4 = 0x0F
KEY_B5 = 0x13
KEY_B6 = 0x17
KEY_B7 = 0x1B
KEY_B8 = 0x1F
KEY_LEFT = 0x04
KEY_RIGHT = 0x08
KEY_LEFT_SPACE = 0x10
KEY_RIGHT_SPACE = 0x18
KEY_ROUTING = 0x20
KEY_RELEASE_MASK = 0x80
# Braille dot mapping
KEY_DOTS = {
KEY_B4: 1,
KEY_B3: 2,
KEY_B2: 3,
KEY_B5: 4,
KEY_B6: 5,
KEY_B7: 6,
KEY_B1: 7,
KEY_B8: 8,
}
# Considered spaces in braille input mode
KEY_SPACES = (KEY_LEFT_SPACE, KEY_RIGHT_SPACE)
class Model(AutoPropertyObject):
"""Extend from this base class to define model specific behavior."""
#: Device identifier, used in the protocol to identify the device
#: @type: string
deviceId = None
#: A generic name that identifies the model/series, used in gesture identifiers
#: @type: string
genericName = None
#: Specific name of this model
#: @type: string
name = None
#: Number of braille cells
#: @type: int
numCells = 0
def __init__(self, display):
super(Model, self).__init__()
# A weak reference to the driver instance, used due to a circular reference between Model and Display
self._displayRef = weakref.ref(display)
def postInit(self):
"""Executed after model initialisation.
Subclasses may extend this method to perform actions on initialization
of the display. Don't use __init__ for this, since the model ID has
not been set, which is needed for sending packets to the display.
"""
def _get__display(self):
"""The L{BrailleDisplayDriver} which initialized this Model instance"""
# self._displayRef is a weakref, call it to get the object
return self._displayRef()
def _get_keys(self):
"""Basic keymap
This returns a basic keymap with sensible defaults for all devices.
Subclasses should override this method to add model specific keys,
or relabel keys. Even if a key isn't available on all devices, add it here
if it would make sense for most devices.
"""
return OrderedDict(
{
# Braille input keys
# Numbered from left to right, might be used for braille input on some models
KEY_B1: "b1",
KEY_B2: "b2",
KEY_B3: "b3",
KEY_B4: "b4",
KEY_B5: "b5",
KEY_B6: "b6",
KEY_B7: "b7",
KEY_B8: "b8",
KEY_LEFT_SPACE: "leftSpace",
KEY_RIGHT_SPACE: "rightSpace",
# Left and right keys, found on Easy Braille and Braille Wave
KEY_LEFT: "left",
KEY_RIGHT: "right",
# Modular/BS80 keypad
0x01: "b12",
0x09: "b13",
0x05: "n0",
0x0D: "b14",
0x11: "b11",
0x15: "n1",
0x19: "n2",
0x1D: "n3",
0x02: "b10",
0x06: "n4",
0x0A: "n5",
0x0E: "n6",
0x12: "b9",
0x16: "n7",
0x1A: "n8",
0x1E: "n9",
},
)
def display(self, cells: List[int]):
"""Display cells on the braille display
This is the modern protocol, which uses an extended packet to send braille
cells. Some displays use an older, simpler protocol. See OldProtocolMixin.
"""
cellBytes: bytes = bytes(cells)
self._display.sendExtendedPacket(
HT_EXTPKT_BRAILLE,
cellBytes,
)
class OldProtocolMixin(object):
"Mixin for displays using an older protocol to send braille cells and handle input"
def display(self, cells: List[int]):
"""Write cells to the display according to the old protocol
This older protocol sends a simple packet starting with HT_PKT_BRAILLE,
followed by the cells. No model ID or length are included.
"""
self._display.sendPacket(HT_PKT_BRAILLE, bytes(cells))
class AtcMixin(object):
"""Support for displays with Active Tactile Control (ATC)"""
def postInit(self):
super(AtcMixin, self).postInit()
log.debug("Enabling ATC")
self._display.atc = True
class TimeSyncFirmnessMixin(object):
"""Functionality for displays that support time synchronization and dot firmness adjustments."""
supportedSettings = (
braille.BrailleDisplayDriver.DotFirmnessSetting(defaultVal=1, minVal=0, maxVal=2, useConfig=False),
)
def postInit(self):
super(TimeSyncFirmnessMixin, self).postInit()
log.debug("Request current display time")
self._display.sendExtendedPacket(HT_EXTPKT_GET_RTC)
log.debug("Request current dot firmness")
self._display.sendExtendedPacket(HT_EXTPKT_GET_FIRMNESS)
def handleTime(self, timeBytes: bytes):
try:
displayDateTime = datetime.datetime(
year=timeBytes[0] << 8 | timeBytes[1],
month=timeBytes[2],
day=timeBytes[3],
hour=timeBytes[4],
minute=timeBytes[5],
second=timeBytes[6],
)
except ValueError:
log.debugWarning("Invalid time/date of Handy Tech display: %r" % timeBytes)
return
localDateTime = datetime.datetime.today()
if abs((displayDateTime - localDateTime).total_seconds()) >= 5:
log.debugWarning("Display time out of sync: %s" % displayDateTime.isoformat())
self.syncTime(localDateTime)
else:
log.debug("Time in sync. Display time %s" % displayDateTime.isoformat())
def syncTime(self, dt: datetime.datetime):
log.debug("Synchronizing braille display date and time...")
# Setting the time uses a swapped byte order for the year.
timeList: List[int] = [
dt.year & 0xFF,
dt.year >> 8,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
]
timeBytes = bytes(timeList)
self._display.sendExtendedPacket(HT_EXTPKT_SET_RTC, timeBytes)
class TripleActionKeysMixin(AutoPropertyObject):
"""Triple action keys
Most Handy Tech models have so called triple action keys. This keys are
on the left and right side of the cells and can be pressed at the top,
at the bottom and in the middle.
"""
def _get_keys(self):
"""Add the triple action keys to the keys property"""
keys = super(TripleActionKeysMixin, self).keys
keys.update(
{
0x0C: "leftTakTop",
0x14: "leftTakBottom",
0x04: "rightTakTop",
0x08: "rightTakBottom",
},
)
return keys
class JoystickMixin(AutoPropertyObject):
"""Joystick
Some Handy Tech models have a joystick, which can be moved left, right, up,
down or clicked on the center.
"""
def _get_keys(self):
"""Add the joystick keys to the keys property"""
keys = super(JoystickMixin, self).keys
keys.update(
{
0x74: "joystickLeft",
0x75: "joystickRight",
0x76: "joystickUp",
0x77: "joystickDown",
0x78: "joystickAction",
},
)
return keys
class StatusCellMixin(AutoPropertyObject):
"""Status cells and routing keys
Some Handy Tech models have four status cells with corresponding routing keys.
"""
def _get_keys(self):
"""Add the status routing keys to the keys property"""
keys = super(StatusCellMixin, self).keys
keys.update(
{
0x70: "statusRouting1",
0x71: "statusRouting2",
0x72: "statusRouting3",
0x73: "statusRouting4",
},
)
return keys
def display(self, cells: List[int]):
"""Display braille on the display with empty status cells
Some displays (e.g. Modular series) have 4 status cells.
These cells need to be included in the braille data, but since NVDA doesn't
support status cells, we just send empty cells.
"""
cells = [0] * 4 + cells
super(StatusCellMixin, self).display(cells)
class ActiveSplitMixin:
"""Mixin for displays supporting ActiveSplit, i.e. dynamic adjustment of number of cells"""
def postInit(self):
super().postInit()
log.debug("Prevent disconnect/reconnect activity for dynamic length adjustment")
self._display.sendExtendedPacket(HT_EXTPKT_NO_RECONNECT)
class ModularConnect88(TripleActionKeysMixin, Model):
deviceId = MODEL_MODULAR_CONNECT_88
genericName = "Modular Connect"
name = "Modular Connect 88"
numCells = 88
class ModularEvolution(AtcMixin, TripleActionKeysMixin, Model):
genericName = "Modular Evolution"
def _get_name(self):
return "{name} {cells}".format(name=self.genericName, cells=self.numCells)
class ModularEvolution88(ModularEvolution):
deviceId = MODEL_MODULAR_EVOLUTION_88
numCells = 88
class ModularEvolution64(ModularEvolution):
deviceId = MODEL_MODULAR_EVOLUTION_64
numCells = 64
class EasyBraille(OldProtocolMixin, Model):
deviceId = MODEL_EASY_BRAILLE
numCells = 40
genericName = name = "Easy Braille"
class ActiveBraille(TimeSyncFirmnessMixin, AtcMixin, JoystickMixin, TripleActionKeysMixin, Model):
deviceId = MODEL_ACTIVE_BRAILLE
numCells = 40
genericName = name = "Active Braille"
class ConnectBraille(TripleActionKeysMixin, Model):
deviceId = MODEL_CONNECT_BRAILLE
numCells = 40
genericName = "Connect Braille"
name = "Connect Braille"
class Actilino(TimeSyncFirmnessMixin, AtcMixin, JoystickMixin, TripleActionKeysMixin, Model):
deviceId = MODEL_ACTILINO
numCells = 16
genericName = name = "Actilino"
class ActiveStar40(TimeSyncFirmnessMixin, AtcMixin, TripleActionKeysMixin, Model):
deviceId = MODEL_ACTIVE_STAR_40
numCells = 40
name = "Active Star 40"
genericName = "Active Star"
class Braillino(TripleActionKeysMixin, OldProtocolMixin, Model):
deviceId = MODEL_BRAILLINO
numCells = 20
genericName = name = "Braillino"
class BrailleWave(OldProtocolMixin, Model):
deviceId = MODEL_BRAILLE_WAVE
numCells = 40
genericName = name = "Braille Wave"
def _get_keys(self):
keys = super(BrailleWave, self).keys
keys.update(
{
0x0C: "escape",
0x14: "return",
KEY_LEFT_SPACE: "space",
},
)
return keys
class BasicBraille(Model):
genericName = "Basic Braille"
def _get_name(self):
return "{name} {cells}".format(name=self.genericName, cells=self.numCells)
class BasicBraillePlus(TripleActionKeysMixin, Model):
genericName = "Basic Braille Plus"
def _get_name(self):
return "{name} {cells}".format(name=self.genericName, cells=self.numCells)
def basicBrailleFactory(numCells, deviceId):
return type(
"BasicBraille{cells}".format(cells=numCells),
(BasicBraille,),
{
"deviceId": deviceId,
"numCells": numCells,
},
)
BasicBraille16 = basicBrailleFactory(16, MODEL_BASIC_BRAILLE_16)
BasicBraille20 = basicBrailleFactory(20, MODEL_BASIC_BRAILLE_20)
BasicBraille32 = basicBrailleFactory(32, MODEL_BASIC_BRAILLE_32)
BasicBraille40 = basicBrailleFactory(40, MODEL_BASIC_BRAILLE_40)
BasicBraille48 = basicBrailleFactory(48, MODEL_BASIC_BRAILLE_48)
BasicBraille64 = basicBrailleFactory(64, MODEL_BASIC_BRAILLE_64)
BasicBraille80 = basicBrailleFactory(80, MODEL_BASIC_BRAILLE_80)
BasicBraille160 = basicBrailleFactory(160, MODEL_BASIC_BRAILLE_160)
BasicBraille84 = basicBrailleFactory(84, MODEL_BASIC_BRAILLE_84)
def basicBraillePlusFactory(numCells, deviceId):
return type(
"BasicBraillePlus{cells}".format(cells=numCells),
(BasicBraillePlus,),
{
"deviceId": deviceId,
"numCells": numCells,
},
)
BasicBraillePlus32 = basicBraillePlusFactory(32, MODEL_BASIC_BRAILLE_PLUS_32)
BasicBraillePlus40 = basicBraillePlusFactory(40, MODEL_BASIC_BRAILLE_PLUS_40)
class BrailleStar(TripleActionKeysMixin, Model):
genericName = "Braille Star"
def _get_name(self):
return "{name} {cells}".format(name=self.genericName, cells=self.numCells)
class BrailleStar40(BrailleStar):
deviceId = MODEL_BRAILLE_STAR_40
numCells = 40
class BrailleStar80(BrailleStar):
deviceId = MODEL_BRAILLE_STAR_80
numCells = 80
class Modular(StatusCellMixin, TripleActionKeysMixin, OldProtocolMixin, Model):
genericName = "Modular"
def _get_name(self):
return "{name} {cells}".format(name=self.genericName, cells=self.numCells)
class Modular20(Modular):
deviceId = MODEL_MODULAR_20
numCells = 20
class Modular40(Modular):
deviceId = MODEL_MODULAR_40
numCells = 40
class Modular80(Modular):
deviceId = MODEL_MODULAR_80
numCells = 80
class Activator(
ActiveSplitMixin,
TimeSyncFirmnessMixin,
AtcMixin,
JoystickMixin,
TripleActionKeysMixin,
Model,
):
deviceId = MODEL_ACTIVATOR
numCells = 40
genericName = name = "Activator"
def _get_keys(self) -> Dict[int, str]:
keys = super().keys
keys.update(
{
0x7A: "escape",
0x7B: "return",
},
)
return keys
class ActivatorPro(
ActiveSplitMixin,
TimeSyncFirmnessMixin,
AtcMixin,
TripleActionKeysMixin,
Model,
):
genericName = "Activator Pro"
def _get_name(self):
return "{name} {cells}".format(name=self.genericName, cells=self.numCells)
def _get_keys(self) -> Dict[int, str]:
keys = super().keys
keys.update(
{
0x7A: "escape",
0x7B: "return",
},
)
return keys
class ActivatorPro64(ActivatorPro):
deviceId = MODEL_ACTIVATOR_PRO_64
numCells = 64
class ActivatorPro80(ActivatorPro):
deviceId = MODEL_ACTIVATOR_PRO_80
numCells = 80
def _allSubclasses(cls):
"""List all direct and indirect subclasses of cls
This function calls itself recursively to return all subclasses of cls.
@param cls: the base class to list subclasses of
@type cls: class
@rtype: [class]
"""
return cls.__subclasses__() + [g for s in cls.__subclasses__() for g in _allSubclasses(s)]
# Model dict for easy lookup
MODELS = {m.deviceId: m for m in _allSubclasses(Model) if hasattr(m, "deviceId")}
# Packet types
HT_PKT_BRAILLE = b"\x01"
HT_PKT_EXTENDED = b"\x79"
HT_PKT_NAK = b"\x7d"
HT_PKT_ACK = b"\x7e"
HT_PKT_OK = b"\xfe"
HT_PKT_OK_WITH_LENGTH = b"\xfd"
HT_PKT_RESET = b"\xff"
HT_EXTPKT_BRAILLE = HT_PKT_BRAILLE
HT_EXTPKT_KEY = b"\x04"
HT_EXTPKT_CONFIRMATION = b"\x07"
HT_EXTPKT_SCANCODE = b"\x09"
HT_EXTPKT_PING = b"\x19"
HT_EXTPKT_SERIAL_NUMBER = b"\x41"
HT_EXTPKT_SET_RTC = b"\x44"
HT_EXTPKT_GET_RTC = b"\x45"
HT_EXTPKT_BLUETOOTH_PIN = b"\x47"
HT_EXTPKT_SET_ATC_MODE = b"\x50"
HT_EXTPKT_SET_ATC_SENSITIVITY = b"\x51"
HT_EXTPKT_ATC_INFO = b"\x52"
HT_EXTPKT_SET_ATC_SENSITIVITY_2 = b"\x53"
HT_EXTPKT_GET_ATC_SENSITIVITY_2 = b"\x54"
HT_EXTPKT_READING_POSITION = b"\x55"
HT_EXTPKT_SET_FIRMNESS = b"\x60"
HT_EXTPKT_GET_FIRMNESS = b"\x61"
HT_EXTPKT_GET_PROTOCOL_PROPERTIES = b"\xc1"
HT_EXTPKT_GET_FIRMWARE_VERSION = b"\xc2"
HT_EXTPKT_NO_RECONNECT = b"\xae"
# HID specific constants
HT_HID_RPT_OutData = b"\x01" # receive data from device
HT_HID_RPT_InData = b"\x02" # send data to device
HT_HID_RPT_InCommand = b"\xfb" # run USB-HID firmware command
HT_HID_RPT_OutVersion = b"\xfc" # get version of USB-HID firmware
HT_HID_RPT_OutBaud = b"\xfd" # get baud rate of serial connection
HT_HID_RPT_InBaud = b"\xfe" # set baud rate of serial connection
HT_HID_CMD_FlushBuffers = b"\x01" # flush input and output buffers
class BrailleDisplayDriver(braille.BrailleDisplayDriver, ScriptableObject):
name = "handyTech"
# Translators: The name of a series of braille displays.
description = _("Handy Tech braille displays")
isThreadSafe = True
supportsAutomaticDetection = True
receivesAckPackets = True
timeout = 0.2
_sleepcounter = 0
_messageWindow = None
_instances = weakref.WeakSet()
@classmethod
def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
driverRegistrar.addUsbDevices(
bdDetect.ProtocolType.SERIAL,
{
"VID_0403&PID_6001", # FTDI chip
"VID_0921&PID_1200", # GoHubs chip
},
)
# Newer Handy Tech displays have a native HID processor
driverRegistrar.addUsbDevices(
bdDetect.ProtocolType.HID,
{
"VID_1FE4&PID_0054", # Active Braille
"VID_1FE4&PID_0055", # Connect Braille
"VID_1FE4&PID_0061", # Actilino
"VID_1FE4&PID_0064", # Active Star 40
"VID_1FE4&PID_0081", # Basic Braille 16
"VID_1FE4&PID_0082", # Basic Braille 20
"VID_1FE4&PID_0083", # Basic Braille 32
"VID_1FE4&PID_0084", # Basic Braille 40
"VID_1FE4&PID_008A", # Basic Braille 48
"VID_1FE4&PID_0086", # Basic Braille 64
"VID_1FE4&PID_0087", # Basic Braille 80
"VID_1FE4&PID_008B", # Basic Braille 160
"VID_1FE4&PID_008C", # Basic Braille 84
"VID_1FE4&PID_0093", # Basic Braille Plus 32
"VID_1FE4&PID_0094", # Basic Braille Plus 40
"VID_1FE4&PID_00A4", # Activator
"VID_1FE4&PID_00A6", # Activator Pro 64
"VID_1FE4&PID_00A8", # Activator Pro 80
},
)
# Some older HT displays use a HID converter and an internal serial interface
driverRegistrar.addUsbDevices(
bdDetect.ProtocolType.HID,
{
"VID_1FE4&PID_0003", # USB-HID adapter
"VID_1FE4&PID_0074", # Braille Star 40
"VID_1FE4&PID_0044", # Easy Braille
},
)
driverRegistrar.addBluetoothDevices(
lambda m: any(
m.id.startswith(prefix)
for prefix in (
"Actilino AL",
"Active Braille AB",
"Active Star AS",
"Basic Braille BB",
"Basic Braille Plus BP",
"Braille Star 40 BS",
"Braillino BL",
"Braille Wave BW",
"Easy Braille EBR",
"Activator",
)
),
)
@classmethod
def getManualPorts(cls):
return braille.getSerialPorts()
_dev: Optional[Union[hwIo.Hid, hwIo.Serial]]
def __new__(cls, *args, **kwargs):
obj = super().__new__(cls, *args, **kwargs)
cls._instances.add(obj)
return obj
def __init__(self, port="auto"):
super().__init__()
self.numCells = 0
self._model = None
self._ignoreKeyReleases = False
self._keysDown = set()
self.brailleInput = False
self._dotFirmness = 1
self._hidSerialBuffer = b""
self._atc = False
for portType, portId, port, portInfo in self._getTryPorts(port):
# At this point, a port bound to this display has been found.
# Try talking to the display.
self.isHid = portType == bdDetect.ProtocolType.HID
self.isHidSerial = portId in USB_IDS_HID_CONVERTER
self.port = port
try:
if self.isHidSerial:
# This is either the standalone HID adapter cable for older displays,
# or an older display with a HID - serial adapter built in
self._dev = hwIo.Hid(port, onReceive=self._hidSerialOnReceive)
# Send a flush to open the serial channel
self._dev.write(HT_HID_RPT_InCommand + HT_HID_CMD_FlushBuffers)
elif self.isHid:
self._dev = hwIo.Hid(port, onReceive=self._hidOnReceive)
else:
self._dev = hwIo.Serial(
port,
baudrate=BAUD_RATE,
parity=PARITY,
timeout=self.timeout,
writeTimeout=self.timeout,
onReceive=self._serialOnReceive,
)
except EnvironmentError:
log.debugWarning("", exc_info=True)
continue
self.sendPacket(HT_PKT_RESET)
for _i in range(3):
# An expected response hasn't arrived yet, so wait for it.
self._dev.waitForRead(self.timeout)
if self.numCells and self._model:
break
if self.numCells:
# A display responded.
if not isinstance(self._model, OldProtocolMixin):
self.sendExtendedPacket(HT_EXTPKT_GET_PROTOCOL_PROPERTIES)
self._dev.waitForRead(self.timeout)
self._model.postInit()
log.info(
"Found {device} connected via {type} ({port})".format(
device=self._model.name,
type=portType,
port=port,
),
)
# Create the message window on the ui thread.
wx.CallAfter(self.createMessageWindow)
break
self._dev.close()
else:
raise RuntimeError("No Handy Tech display found")
@classmethod
def createMessageWindow(cls):
if cls._messageWindow:
return
try:
cls._sleepcounter = 0
cls._messageWindow = InvisibleDriverWindow()
except WindowsError:
log.debugWarning("", exc_info=True)
@classmethod
def destroyMessageWindow(cls):
if len(cls._instances) > 1:
# When switching from automatic detection to manual display selection or vice versa,
# there could exist more than one driver instance at a time.
# Ensure that the message window won't be destroyed in these cases.
return
cls._sleepcounter = 0
try:
cls._messageWindow.destroy()
except WindowsError:
log.debugWarning("", exc_info=True)
cls._messageWindow = None
def goToSleep(self):
BrailleDisplayDriver._sleepcounter += 1
if self._dev is not None:
# Must sleep before and after closing to ensure the device can be reconnected.
time.sleep(self.timeout)
self._dev.close()
self._dev = None
time.sleep(self.timeout)
def wakeUp(self):
if BrailleDisplayDriver._sleepcounter > 0:
BrailleDisplayDriver._sleepcounter -= 1
if BrailleDisplayDriver._sleepcounter > 0: # Still not zero after decrementing
return
# Might throw if device no longer exists.
# We leave it to autodetection to grab it when it reappears.
if self.isHidSerial:
# This is either the standalone HID adapter cable for older displays,
# or an older display with a HID - serial adapter built in
self._dev = hwIo.Hid(self.port, onReceive=self._hidSerialOnReceive)
# Send a flush to open the serial channel
self._dev.write(HT_HID_RPT_InCommand + HT_HID_CMD_FlushBuffers)
elif self.isHid:
self._dev = hwIo.Hid(self.port, onReceive=self._hidOnReceive)
else:
self._dev = hwIo.Serial(
self.port,
baudrate=BAUD_RATE,
parity=PARITY,
timeout=self.timeout,
writeTimeout=self.timeout,
onReceive=self._serialOnReceive,
)
def terminate(self):
try:
# Make sure this is called on the ui thread.
wx.CallAfter(self.destroyMessageWindow)
super().terminate()
finally:
# We must sleep before closing the connection as not doing this can leave the display in a bad state where it can not be re-initialized.
# This has been observed for Easy Braille displays.
time.sleep(self.timeout)
# Make sure the device gets closed.
self._dev.close()
# We also must sleep after closing, as it sometimes takes some time for the device to disconnect.
# This has been observed for Active Braille displays.
time.sleep(self.timeout)
def _get_supportedSettings(self):
settings = [
braille.BrailleDisplayDriver.BrailleInputSetting(),
]
if self._model:
# Add the per model supported settings to the list.
for cls in self._model.__class__.__mro__:
if hasattr(cls, "supportedSettings"):
settings.extend(cls.supportedSettings)
return settings
def _get_atc(self):
return self._atc
def _set_atc(self, state):
if self._atc is state:
return
if isinstance(self._model, AtcMixin):
self.sendExtendedPacket(HT_EXTPKT_SET_ATC_MODE, boolToByte(state))
else:
log.debugWarning("Changing ATC setting for unsupported device %s" % self._model.name)
# Regardless whether this setting is supported or not, we want to safe its state.
self._atc = state
def _get_dotFirmness(self):
return self._dotFirmness
def _set_dotFirmness(self, value):
if self._dotFirmness is value:
return
if isinstance(self._model, TimeSyncFirmnessMixin):
self.sendExtendedPacket(HT_EXTPKT_SET_FIRMNESS, intToByte(value))
else:
log.debugWarning("Changing dot firmness setting for unsupported device %s" % self._model.name)
# Regardless whether this setting is supported or not, we want to safe its state.
self._dotFirmness = value
def sendPacket(self, packetType: bytes, data: bytes = b""):
if BrailleDisplayDriver._sleepcounter > 0:
return
if self.isHid:
self._sendHidPacket(packetType + data)
else:
self._dev.write(packetType + data)
def sendExtendedPacket(self, packetType: bytes, data: bytes = b""):
if BrailleDisplayDriver._sleepcounter > 0:
log.debug("Packet discarded as driver was requested to sleep")
return
packetBytes: bytes = b"".join(
[
intToByte(len(data) + len(packetType)),
packetType,
data,
b"\x16",
],
)
if self._model:
packetBytes = self._model.deviceId + packetBytes
self.sendPacket(HT_PKT_EXTENDED, packetBytes)
def _sendHidPacket(self, packet: bytes):
assert self.isHid
maxBlockSize = self._dev._writeSize - 3
# When the packet length exceeds C{writeSize}, the packet is split up into several packets.
# They contain C{HT_HID_RPT_InData}, the length of the data block,
# the data block itself and a terminating null character.
for offset in range(0, len(packet), maxBlockSize):
block = packet[offset : offset + maxBlockSize]
hidPacket = HT_HID_RPT_InData + intToByte(len(block)) + block + b"\x00"
self._dev.write(hidPacket)
def _handleKeyRelease(self):
if self._ignoreKeyReleases or not self._keysDown:
return
# The first key released executes the key combination.
try:
inputCore.manager.executeGesture(
InputGesture(self._model, self._keysDown, self.brailleInput),
)
except inputCore.NoInputGestureAction:
pass
# Any further releases are just the rest of the keys in the combination
# being released, so they should be ignored.
self._ignoreKeyReleases = True
def _hidOnReceive(self, data: bytes):
# data contains the entire packet.
stream = BytesIO(data)
htPacketType = data[2:3]
# Skip the header, so reading the stream will only give the rest of the data
stream.seek(3)
self._handleInputStream(htPacketType, stream)
def _hidSerialOnReceive(self, data: bytes):
# The HID serial converter wraps one or two bytes into a single HID packet
hidLength = data[1]
self._hidSerialBuffer += data[2 : (2 + hidLength)]