Support Braille displays that conform to the HID Braille standard (#12523)
Braille Display devices allow being controlled by Screen Readers using a variety of connections such as Serial, USB and Bluetooth. The protocols used over these channels have traditionally been Braille Display manufacturer specific.
This has meant that in order for a Screen Reader to support a particular Braille display, it must have specific logic in the Screen Reader that knows how to speak the required device-specific protocol. Further to this, On Windows at least, an OS-level driver provided by the Braille Display manufacturer must also be installed by the user in order for the computer to detect and communicate with the device.
With the introduction of the HID (Human Interface Device) standard for USB (and later Bluetooth), it became possible to remove the need for the required OS-level device driver on Windows if the Braille Display manufacturer exposed the device as a custom HID device, however the Screen Reader still needed device-specific code, as being custom HID only simplified the low-level bytes communication, but did not standardize what those bytes actually meant.
In 2018, The HID specification was extended to define the concept of a Braille Display device, including setting of braille cells, and the standardizing of common buttons found on Braille displays such as routing keys, Braille dot input keys, braille space and panning keys.
NVDA should add specific support for a Braille HID device, so that Braille Displays that support this standard can automatically be detected and usable by NVDA.
Description of how this pull request fixes the issue:
• Adds hidpi.py which contains all necessary types and defines from Windows' hidpi.h (the HID parser header).
• Some HidP types that were previously added to hwIo.py have been moved to hidpi.py.
• the hwIo.Hid class has been extended to include some useful properties such as caps (HidP_GetCaps), inputButtonCaps (HidP_GetButtonCaps), inputValueCaps, and outputValueCaps (HidP_GetValueCaps).
• a new brailleDisplayDriver (brailleDisplayDrivers/hid.py) has been added which supports Braille displays that use the Braille HID specification.
◦ On construction:
◾ It opens the given device port by instantiating an hwIo.Hid object.
◾ It ensures the Hid device is truly a Braille display by checking that the HIDP_CAPS.UsagePage of the HID device's top-level collection is set to HID_USAGE_PAGE_BRAILLE (0x41).
◾ It finds the correct output HIDP_VALUE_CAPS structure which represents the array of braille cells. I.e. the Usage ID is either EIGHT_DOT_BRAILLE_CELL or six_dot_braille_cell. The ReportCount member of this struct states the number of cells for the device. This structure is also saved off as it is later needed when writing braille to the display.
◾ It collects all input button HIDP_VALUE_CAPS structures supported by the device, and stores them in a dictionary, keyed by their DataIndex member.
◾ If the HIDP_VALUE_CAPS structure represents a range of data indices, then the HIDP_VALUE_CAPS structure is added to the dictionary for each data index in that range. For example a device may expose the Braille dot input keys this way.
◾ Each value for the dictionary is a tuple containing the HIDP_VALUE_CAPS structure, and a calculated relative DataIndex from the first HIDP_VALUE_CAPS structure with the same LinkCollection member. Examples of collections are Router_set_1 or Braille_row. We certainly need this relative index for routing keys.
◦ The BrailleDisplayDriver.display method (for writing cells to the device) works as follows:
◾ Creates a HID output report, setting the report ID to the value of the ReportID member of the cell HIDP_VALUE_CAPS structure found at construction.
◾ Calls HidP_SetUsageValueArray to place the data (braille cell dot patterns) into the report at the appropriate place, Using the Usage ID and collection number etc from the cell HIDP_VALUE_CAPS structure.
◾ Sends the report to the Braille display using WriteFile. The number of bytes written will be the value of HIDP_CAPS.OutputReportByteLength.
◦ the BrailleDisplayDriver receives input from the Braille display by providing a callback to the hwIo.Hid object. The Hid object uses overlapped IO and ReadFile to continuously read blocks of data of the device's HID input report size (HIDP_CAPS.InputReportByteLength).
◦ the BrailleDisplayDriver's _hidOnReceive callback does the following:
◾ Treating the received data as a HID input report, HidP_GetData is used to extract all HIDP_DATA structures from the report. these represent the state of all input buttons and other controls.
◾ For each data item that represents a input button and is currently on, the value of the data item's DataIndex member is recorded in a keys list (keys currently being pressed). As long as this list keeps growing, all these keys are treated as being a part of the upcoming gesture. If the list decreases, then these keys are used to create an NVDA InputGesture which is executed.
◦ This BrailleDisplayDriver's InputGesture class processes the given data indices as follows:
◾ It looks up the correct input button HIDP_VALUE_CAPS structure and relative index in collection value for the data index, using the dictionary created in the BrailleDisplayDriver's construction.
◾ If the HIDP_VALUE_CAPS structure represents a range of buttons, it calculates the correct Usage ID using the DataIndex value and the DataIndexMin and UsageMin HIDP_VALUE_CAPS members.
◾ If the Usage ID is one of the BRAILLE_KEYBOARD_DOT_* values, it sets the appropriate bit in the InputGesture's dots member.
◾ If the Usage ID is one of the BRAILLE_KEYBOARD_SPACE, BRAILLE_KEYBOARD_LEFT_SPACE or BRAILLE_KEYBOARD_RIGHT_SPACE, it sets the InputGesture's space member to true.
◾ If the Usage ID is ROUTER_KEY, then the InputGesture's routingIndex member is set using the calculated relativeIndexInCollection value for this data index.
◾ The generic gesture name is calculated from the Usage ID's enum name, converted to camelCase, with prefixes such as BRAILLE_KEYBOARD_ and Braille_ removed. E.g. BRAILLE_PAN_LEFT becomes panLeft. And for router collections, the router collection name is prefixed E.g. routerSet1_routerKey.
◦ A gestureMap has been provided for this BrailleDisplayDriver, which is roughly based on the Brailliant B driver, for all gestures that are supported by Braille HID.
• hwPortUtils._getHidInfo now includes the device's top-level collection's UsagePage value in the info it returns. Note that this does mean now that all HID devices are opened to get this, where as before we only opened Bluetooth devices.
• When bdDetect looks up an appropriate BrailleDisplayDriver for a USB or Bluetooth device, or when it looks up compatible USB or Bluetooth devices for a given BrailleDisplayDriver, if the USB or Bluetooth device is a HID device and its to-level collection's UsagePage value is HID_USAGE_PAGE_BRAILLE (0x41), then the hid BrailleDisplayDriver is chosen before any other match. Thus if a HID device reports it is a HID braille display, the hid BrailleDisplayDriver will automatically be used.