Skip to content

Add Bluetooth Low Energy support for DotPad braille displays#19122

Draft
bramd wants to merge 6 commits into
nvaccess:masterfrom
bramd:dotpad-ble
Draft

Add Bluetooth Low Energy support for DotPad braille displays#19122
bramd wants to merge 6 commits into
nvaccess:masterfrom
bramd:dotpad-ble

Conversation

@bramd

@bramd bramd commented Oct 20, 2025

Copy link
Copy Markdown
Contributor

DotPad braille displays support both USB Serial and Bluetooth Low Energy (BLE) connections. NVDA previously only supported USB connections.

  • DotPad displays can now be automatically detected and connected via BLE
  • Users can manually select specific BLE devices from the braille settings dialog
  • BLE device scanning starts automatically when opening braille settings
  • Device list can be refreshed by switching to another display and back
  • When no BLE devices are found, a helpful message guides users to refresh
  • Add Bleak 1.1.0 dependency for BLE support
  • Implement asyncio event loop integration for async BLE operations
  • Create hwIo.ble module with Scanner and Ble classes
  • Extend bdDetect to support BLE device detection alongside USB/Bluetooth
  • Add BLE devices to getPossiblePorts() for manual selection in settings dialog
  • Update driverHasPossibleDevices() to include BLE devices
  • Implemented real-time device detection using extension points

DotPad Driver:

  • Registered BLE devices via driverRegistrar.addBleDevices() with name-based matching
  • Serialized BLE devices as "name@address" for unique identification
  • Overrode check() method to show driver when BLE adapter available
  • Added BLE connection support in _tryConnect() method

GUI Integration:

  • Start BLE scanner when braille settings dialog opens
  • Stop scanner on dialog close (unless background detection active)
  • Show "(No devices found - switch to another display and back to refresh)" when no ports available
  • Format BLE devices as "Bluetooth: DeviceName" in port dropdown

Link to issue number:

Fixes #18584

Summary of the issue:

DotPad braille displays support both USB Serial and Bluetooth Low Energy (BLE) connections. NVDA previously only supported USB connections.

Description of user facing changes:

  • DotPad displays can now be automatically detected and connected via BLE
  • Users can manually select specific BLE devices from the braille settings dialog
  • BLE device scanning starts automatically when opening braille settings
  • Device list can be refreshed by switching to another display and back
  • When no BLE devices are found, a helpful message guides users to refresh

Description of developer facing changes:

  • Add Bleak 1.1.0 dependency for cross-platform BLE support
  • Implement asyncio event loop integration for async BLE operations
  • Create hwIo.ble module with
    • scanner, a singleton to use as BLE scanner by NVDA core or other add-ons
    • Ble: a hwIo compatible class to communicate with BLE devices
  • Add an asyncioEventLoop module with an asyncio event loop and a utility method to schedule coroutines on that loop
  • Extend bdDetect to support BLE device detection alongside USB/Bluetooth
  • Add BLE devices to getPossiblePorts() for manual selection in settings dialog
  • Update driverHasPossibleDevices() to include BLE devices

Description of development approach:

Added Bleak for BLE communication. Since Bleak is an asyncio library, I had to implement an asyncio event loop as well. I chose to run this on it's separate thread and not integrate it in NVDA's WX main loop in any way.

bdDetect has been modified to scan for and match BLE devices with their drivers. Since with BLE in Windows the app is responsible for all the scanning and connecting, the scanner starts as soon as the braille display selection is opened. Otherwise, we never would have devices to connect to in the port selection list. If there are no ports, the ports list shows a message to help the user that they can switch to another display and back to refresh the list.

The DotPad driver has been modified to accept BLE devices to connect to. BLE devices are stored in the configuration as ble:name@address, since we need the address to connect if there is no scanner result. If there is a scanner result, we can match on name as well.

The onReceive method of the driver has been altered so it accepts both input one byte at a time (for serial) or as whole packets (BLE).

The BLE scanner implements a new Action (extension point) that fires when a BLE device is discovered. Therefore, connection after turning on the device should be very quickly established.

Testing strategy:

Wrote extensive unit tests and tested with actual hardware.

Things to test:

  1. Automatic detection on BLE and USB
  2. Turn DotPad on before NVDA starts
  3. Start NVDA, set display to auto and turn on Dot Pad
  4. Switching to USB when cable is plugged in and devices is already connected over BLE
  5. Manual selection of a specific BLE device, saving the configuration and restarting NVDA, the device should connect immediately after restart

Known issues with pull request:

Since BLE does not require any pairing, the bdDetect system now connects to any DotPad it discovers. Since DotPad is disabled for automatic detection by default and users can set a specific BLE device to connect to, I think this is acceptable. If not, we would have to implement a GUI for selecting devices and keep a list of selected devices that are allowed to be discovered automatically.

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

@bramd
bramd marked this pull request as ready for review October 20, 2025 16:00
@bramd
bramd requested review from a team as code owners October 20, 2025 16:00
Comment thread source/bdDetect.py
log.debug("Starting BLE scanner for background scan")
hwIo.ble.scanner.start()
# Give the scanner some time to get initial results
time.sleep(0.2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we somehow wait non-blocking here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was assuming that a background scan was always running on the hwIo thread and that a blocking sleep wouldn't matter that much. In theory, Windows is already scanning and caching results, but depending on hardware and BLE stack implementation we might end up with 0 results without a wait. So I found it more acceptable to have a small delay in a background scan then having no results and only having a chance to find BLE devices in the next scanning round. Especially when having Bluetooth classic devices that can match in a background scan and having Windows take 10-20 seconds to get a timeout when trying to connect a Bluetooth serial port, I think a 0.2 sec delay is acceptable.

If my assumptions are invalid or you see a way to wait non-blocking and still get results in the same scanning round, I would like to hear your thoughts.

Comment thread source/bdDetect.py
@param bluetooth: Whether the handler is expected to yield USB devices.
@param bluetooth: Whether the handler is expected to yield Bluetooth devices.
@type bluetooth: bool
@param ble: Whether the handler is expected to yield BLE devices.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered reusing the bluetooth parameter? Can't you just handle BLE in case a driver is enabled that supports BLE?
I assume scanning for BLE can be a bit costly. It makes not that much sense to scan for these devices when I don't have a device that supports it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered it, but chose a separate ble parameter because:

  1. BLE can connect to devices without Windows pairing them first and there might be a wish to disable the feature for example in an environment with any BLE braille displays, where Bluetooth classic pairing would still be acceptable for automatic detection. Note that we do not expose that in the UI at the moment
  2. Windows' documentation is unclear about the cost of BLE scanning. It seems Windows is doing some kind of scanning and caching anyway, but this might also be dependent on the hardware and Bluetooth drivers/stack and is hard to test without clear documentation and not much Bluetooth adapters to test with

However, if there is no wish to disable BLE separately, I'd be fine with merging the ble/bluetooth parameters.

Comment thread source/braille.py Outdated
Comment thread source/braille.py Outdated
ports[portKey] = portName
except Exception:
# If BLE scanning fails, continue without BLE devices
pass

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silently discarding a bare except feels like bad practice. Could you either make the exception more specific or log a debug warning or the like?

Comment thread source/braille.py Outdated
if isinstance(port, bdDetect.DeviceMatch):
yield port
elif isinstance(port, str):
# Check if this is a specific BLE device port (format: "ble:DeviceName@Address" or legacy "ble:DeviceName")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you split this out into a helper function?

try:
self._processPacket(packet[4:])
except Exception:
log.error("Error processing packet", exc_info=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will re-enter the loop, right? Can't this create an infinite loop we'll never break from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before processing the packet, it's removed from the buffer. So in most cases we would end up with an empty buffer and the loop would exit. If there somehow is a few bytes left in the buffer it will either be a valid packet and processed as such, or the loop will exit because there are not enough bytes. Where/how do you see the risk of the loop not being exited?

else:
# No ports available - show helpful message
# Translators: Message shown when no devices are available for a braille display
noDevicesMessage = _("(No devices found - switch to another display and back to refresh)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could there be a better UX for this? Can't you just refocus or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify this? Do you want the ports list to be enabled so people can read this message better or do you want that the ports list dynamically updates in realtime if devices are discovered?

Comment thread source/hwIo/ble/_io.py Outdated
Comment thread source/hwIo/ble/_scanner.py Outdated
@LeonarddeR
LeonarddeR requested a review from Copilot October 20, 2025 19:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds Bluetooth Low Energy (BLE) support for DotPad braille displays, enabling automatic detection and manual selection of BLE-connected devices alongside existing USB support.

Key changes:

  • Introduced Bleak 1.1.0 dependency and asyncio event loop infrastructure for BLE communication
  • Extended bdDetect system to handle BLE device scanning and matching in parallel with USB/Bluetooth
  • Updated GUI to automatically start BLE scanning when braille settings dialog opens, with user guidance for device refresh

Reviewed Changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
user_docs/en/userGuide.md Added BLE connection documentation and usage instructions for DotPad
user_docs/en/changes.md Documented BLE support as a new feature in changelog
tests/unit/test_hwIo_ble.py Added comprehensive unit tests for Scanner, Ble I/O, and device lookup
tests/unit/test_bdDetect.py Extended tests to cover BLE device registration and matching
tests/unit/brailleDisplayDrivers/test_dotPad.py Added buffered receive tests for serial and BLE packet handling
source/hwIo/ble/_scanner.py Implemented BLE scanner wrapper around Bleak with extension point for device discovery
source/hwIo/ble/_io.py Created Ble I/O class for BLE communication with MTU-aware writes and notification handling
source/hwIo/ble/init.py Exposed scanner singleton and device lookup utility functions
source/gui/settingsDialogs.py Modified braille settings dialog to start/stop BLE scanner and display helpful messages
source/core.py Integrated asyncio event loop initialization and termination
source/brailleDisplayDrivers/dotPad/driver.py Enhanced driver to support BLE connections with buffered packet processing
source/brailleDisplayDrivers/dotPad/defs.py Added BLE service and characteristic UUIDs
source/braille.py Extended device detection and port enumeration to include BLE devices
source/bdDetect.py Added BLE scanning, real-time discovery notifications, and device matching
source/asyncioEventLoop.py Created asyncio event loop module for running async operations
pyproject.toml Added Bleak dependency and WinRT package exceptions

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread source/asyncioEventLoop.py Outdated
Comment thread source/hwIo/ble/_io.py Outdated
Comment thread source/hwIo/ble/_io.py Outdated
@SaschaCowley SaschaCowley added the conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. label Oct 21, 2025
@bramd

bramd commented Oct 21, 2025

Copy link
Copy Markdown
Contributor Author

@SaschaCowley I see you fixed the Chrome system tests yesterday, but in this PR the startup/shutdown and installer tests are still failing. Is this expected or could it indicate a problem introduced in this PR?

@SaschaCowley

Copy link
Copy Markdown
Member

Hi @bramd, sometimes these tests do just fail. However the fact that the tests on Windows 2022 and 2025 failed in the same way is suspicious. I've triggered the tests to rerun to see if it was just a fluke

@bramd

bramd commented Oct 29, 2025

Copy link
Copy Markdown
Contributor Author

Hi @bramd, sometimes these tests do just fail. However the fact that the tests on Windows 2022 and 2025 failed in the same way is suspicious. I've triggered the tests to rerun to see if it was just a fluke

Unfortunately, they still seem to fail. I'm trying to get a VM going with NVDA source to test properly without a Bluetooth adapter, but have some issues getting the build environment set up for now. Pending the failing tests, I think this is not ready for merge. However, I would appreciate it if you can at least test this on your hardware, since there are not much users with Dot Pad hardware to try this PR.

@SaschaCowley

Copy link
Copy Markdown
Member

However, I would appreciate it if you can at least test this on your hardware, since there are not much users with Dot Pad hardware to try this PR.

I will try out the PR tomorrow and let you know how it goes.

Just so you're aware, this PR may take longer for us to get to as it is quite large, so I want to set aside a while to sit down with it and properly digest it. We definitely have not forgotten about it though :)

@SaschaCowley

Copy link
Copy Markdown
Member

Hi @bramd,

I have just tested this PR with our DotPad. I performed the current steps:

  1. Build and ran this PR from source.
  2. Switched on the DotPad.
  3. Opened the braille display selection dialog, selected DotPad, and selected bluetooth as the port.
  4. Pressed Ok.

This caused the DotPad to emit two long buzzes, pause for a few seconds, then emit two more long buzzes, and NVDA to report an error connecting.

IO - speech.speech.speak (14:28:11.551) - MainThread (8232):
Speaking [LangChangeCommand ('en_GB'), 'Log fragment start position marked, press again to copy to clipboard']
IO - inputCore.InputManager.executeGesture (14:28:12.765) - winInputHook (10844):
Input: kb(desktop):NVDA+control+a
DEBUG - gui.settingsDialogs.__new__ (14:28:12.775) - MainThread (8232):
Creating new settings dialog (multiInstanceAllowed:False). State of _instances {}
DEBUG - gui.contextHelp.bindHelpEvent (14:28:12.778) - MainThread (8232):
Did context help binding for BrailleDisplaySelectionDialog
DEBUG - gui.contextHelp.bindHelpEvent (14:28:12.783) - MainThread (8232):
Did context help binding for LabeledControlHelper.__init__.<locals>.WxCtrlWithEnableEvnt
DEBUG - gui.contextHelp.bindHelpEvent (14:28:12.787) - MainThread (8232):
Did context help binding for LabeledControlHelper.__init__.<locals>.WxCtrlWithEnableEvnt
DEBUG - gui.contextHelp.bindHelpEvent (14:28:12.794) - MainThread (8232):
Did context help binding for LabeledControlHelper.__init__.<locals>.WxCtrlWithEnableEvnt
DEBUGWARNING - hwPortUtils.listUsbDevices (14:28:12.796) - MainThread (8232):
Couldn't get DEVPKEY_Device_BusReportedDeviceDesc for {'hardwareID': 'USB\\VID_10AB&PID_9309&REV_0001', 'usbID': 'VID_10AB&PID_9309', 'devicePath': '\\\\?\\usb#vid_10ab&pid_9309#7&2f3a9896&0&1#{a5dcbf10-6530-11d2-901f-00c04fb951ed}'}: [WinError 1168] Element not found.
DEBUGWARNING - braille.getDisplayList (14:28:12.797) - MainThread (8232):
Braille display driver albatross reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.797) - MainThread (8232):
Braille display driver alva reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.798) - MainThread (8232):
Braille display driver baum reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.798) - MainThread (8232):
Braille display driver brailleNote reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.798) - MainThread (8232):
Braille display driver brailliantB reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.798) - MainThread (8232):
Braille display driver brltty reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.799) - MainThread (8232):
Braille display driver eurobraille reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.799) - MainThread (8232):
Braille display driver freedomScientific reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.799) - MainThread (8232):
Braille display driver handyTech reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.799) - MainThread (8232):
Braille display driver hidBrailleStandard reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.799) - MainThread (8232):
Braille display driver hims reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.800) - MainThread (8232):
Braille display driver nattiqbraille reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.800) - MainThread (8232):
Braille display driver nlseReaderZoomax reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.800) - MainThread (8232):
Braille display driver seikantk reports as unavailable, excluding
DEBUGWARNING - braille.getDisplayList (14:28:12.800) - MainThread (8232):
Braille display driver superBrl reports as unavailable, excluding
IO - speech.speech.speak (14:28:12.889) - MainThread (8232):
Speaking [LangChangeCommand ('en_GB'), 'Select Braille Display', 'dialog', CancellableSpeech (still valid)]
IO - speech.speech.speak (14:28:12.891) - MainThread (8232):
Speaking [LangChangeCommand ('en_GB'), 'Braille display:', 'combo box', 'Automatic', 'collapsed', 'Alt+', CharacterModeCommand(True), 'd', CharacterModeCommand(False), CancellableSpeech (still valid)]
IO - inputCore.InputManager.executeGesture (14:28:14.744) - winInputHook (10844):
Input: kb(desktop):d
DEBUGWARNING - hwPortUtils.listUsbDevices (14:28:14.747) - MainThread (8232):
Couldn't get DEVPKEY_Device_BusReportedDeviceDesc for {'hardwareID': 'USB\\VID_10AB&PID_9309&REV_0001', 'usbID': 'VID_10AB&PID_9309', 'devicePath': '\\\\?\\usb#vid_10ab&pid_9309#7&2f3a9896&0&1#{a5dcbf10-6530-11d2-901f-00c04fb951ed}'}: [WinError 1168] Element not found.
IO - speech.speech.speak (14:28:14.769) - MainThread (8232):
Speaking [LangChangeCommand ('en_GB'), 'DotPad Braille / Tactile Graphic display']
DEBUG - hwIo.ble._scanner.Scanner._onDeviceAdvertised (14:28:17.030) - Thread-2 (run_forever) (12744):
Discovered BLE device: 3E:F6:98:02:9B:9A
IO - inputCore.InputManager.executeGesture (14:28:17.161) - winInputHook (10844):
Input: kb(desktop):enter
DEBUG - hwIo.ble.findDeviceByAddress (14:28:17.164) - MainThread (8232):
Searching for BLE device with address 34:81:F4:45:CD:21
DEBUG - hwIo.ble.findDeviceByAddress (14:28:17.164) - MainThread (8232):
Found BLE device 34:81:F4:45:CD:21 in existing results
INFO - hwIo.ble._io.Ble.__init__ (14:28:17.164) - MainThread (8232):
Connecting to DotPad320A_CD21 (34:81:F4:45:CD:21)
DEBUGWARNING - brailleDisplayDrivers.dotPad.driver.BrailleDisplayDriver._tryConnect (14:28:19.170) - MainThread (8232):
Failed to connect
Traceback (most recent call last):
  File "asyncioEventLoop.py", line 83, in runCoroutineSync
    return future.result(timeout)
           ~~~~~~~~~~~~~^^^^^^^^^
  File "C:\Users\SaschaCowley\AppData\Local\Programs\Python\Python313\Lib\concurrent\futures\_base.py", line 458, in result
    raise TimeoutError()
TimeoutError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "brailleDisplayDrivers\dotPad\driver.py", line 350, in _tryConnect
    self._dev = hwIo.ble.Ble(
                ~~~~~~~~~~~~^
    	device=device,  # Can be BLEDevice or str
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<4 lines>...
    	onReceive=self._onReceive,
     ^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "hwIo\ble\_io.py", line 123, in __init__
    runCoroutineSync(self._initAndConnect(), timeout=CONNECT_TIMEOUT_SECONDS)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "asyncioEventLoop.py", line 87, in runCoroutineSync
    raise TimeoutError(f"Coroutine execution timed out after {timeout} seconds") from e
TimeoutError: Coroutine execution timed out after 2 seconds
ERROR - braille.BrailleHandler.setDisplayByName (14:28:19.172) - MainThread (8232):
Error initializing display driver 'dotPad'
Traceback (most recent call last):
  File "braille.py", line 2784, in setDisplayByName
    self._setDisplay(newDisplayClass, isFallback=isFallback, detected=detected)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "braille.py", line 2855, in _setDisplay
    newDisplay = self._switchDisplay(oldDisplay, newDisplayClass, **kwargs)
  File "braille.py", line 2825, in _switchDisplay
    extensionPoints.callWithSupportedKwargs(newDisplay.__init__, **kwargs)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "extensionPoints\util.py", line 230, in callWithSupportedKwargs
    return func(*boundArguments.args, **boundArguments.kwargs)
  File "brailleDisplayDrivers\dotPad\driver.py", line 325, in __init__
    raise RuntimeError("No DotPad device found")
RuntimeError: No DotPad device found
DEBUGWARNING - Python warning (14:28:19.189) - MainThread (8232):
D:\projects\nvda-pr\source\gui\message.py:129: DeprecationWarning: gui.message.messageBox is deprecated. Use gui.message.MessageDialog instead.
  warnings.warn(
DEBUG - gui.contextHelp.bindHelpEvent (14:28:19.191) - MainThread (8232):
Did context help binding for MessageDialog
DEBUG - gui.message.MessageDialog.ShowModal (14:28:19.201) - MainThread (8232):
Adding <gui.message.MessageDialog object at 0x00000172028DD130> to instances.
DEBUG - gui.message.MessageDialog.ShowModal (14:28:19.202) - MainThread (8232):
Showing <gui.message.MessageDialog object at 0x00000172028DD130> as modal
IO - speech.speech.speak (14:28:19.224) - MainThread (8232):
Speaking [LangChangeCommand ('en_GB'), 'Braille Display Error', 'dialog', 'Could not load the dotPad display.', CancellableSpeech (still valid)]
IO - speech.speech.speak (14:28:19.226) - MainThread (8232):
Speaking [LangChangeCommand ('en_GB'), 'OK', 'button', CancellableSpeech (still valid)]
IO - inputCore.InputManager.executeGesture (14:28:27.470) - winInputHook (10844):
Input: kb(desktop):enter
DEBUG - gui.message.MessageDialog._onButtonEvent (14:28:27.473) - MainThread (8232):
Got button event on id=5100
DEBUG - gui.message.MessageDialog._onCloseEvent (14:28:27.478) - MainThread (8232):
Queueing <gui.message.MessageDialog object at 0x00000172028DD130> for destruction
DEBUG - gui.message.MessageDialog._onCloseEvent (14:28:27.478) - MainThread (8232):
Removing <gui.message.MessageDialog object at 0x00000172028DD130> from instances.
IO - speech.speech.speak (14:28:27.500) - MainThread (8232):
Speaking [LangChangeCommand ('en_GB'), 'Select Braille Display', 'dialog', CancellableSpeech (still valid)]
IO - speech.speech.speak (14:28:27.504) - MainThread (8232):
Speaking [LangChangeCommand ('en_GB'), 'Braille display:', 'combo box', 'DotPad Braille / Tactile Graphic display', 'collapsed', 'Alt+', CharacterModeCommand(True), 'd', CharacterModeCommand(False), CancellableSpeech (still valid)]
IO - inputCore.InputManager.executeGesture (14:28:33.567) - winInputHook (10844):
Input: kb(desktop):NVDA+control+shift+f1

@seanbudd

seanbudd commented Nov 3, 2025

Copy link
Copy Markdown
Member

it seems like this is causing serious system test errors around NVDA exiting safely

@seanbudd
seanbudd marked this pull request as draft November 3, 2025 02:23
@bramd

bramd commented Nov 4, 2025

Copy link
Copy Markdown
Contributor Author

Just so you're aware, this PR may take longer for us to get to as it is quite large, so I want to set aside a while to sit down with it and properly digest it. We definitely have not forgotten about it though :)

No problem, I'm also still having weird issues (not related to this PR) building on my test machine without a Bluetooth adapter and would like to do a good test run on such a machine. That might also give some more insight on the failing system tests. For me this work is partly ported from what was in the Dot Pad add-on and partly rewritten to better fit NVDA core or because I saw room for improvement since the last iteration. Given the complexity of introducing asyncio and the Bleak library and touching quite some points in bdDetect I really appreciate a thorough code review on this. I've quite some time to work on this the coming weeks, so I should be able to address comments quickly.

@bramd

bramd commented Nov 4, 2025

Copy link
Copy Markdown
Contributor Author

[...]
This caused the DotPad to emit two long buzzes, pause for a few seconds, then emit two more long buzzes, and NVDA to report an error connecting.

The first 2 buzzes are expected. The Dot Pad double buzzes every time a BLE connection is made or a connection is closed. So the second buzzes you see match the closing of the connection after the driver fails to initialize.

Could you please try the following:

  1. Enable hwIo debug logging in the advanced settings, logging categories. This will give you a raw dump of every BLE packet the driver sends/receives. Based on your log somehow the connection is not initialized correctly so I don't expect any packets to see now, but it helps you debugging
  2. There is a hardware limitation where the BLE interface may not work correctly if a USB cable is plugged in the data port (left USB port), so please ensure there is no cable plugged in that port
  3. Connect a charger to the power port (on the right) or press and hold panLeft+panRight to get battery state, it will buzz a few times to indicate the battery percentage, with 2 quick buzzes it's almost empty, 5 buzzes is fully charged, with an almost empty battery the BLE connection might drop immediately
  4. Since your unit is a quite early one, it might help to upgrade the firmware. Use a Chromium-based browser and go to https://support.dotincorp.com/update/firmware/connect
  5. You could increase the CONNECT_TIMEOUT constant in ble._io just to see if that makes any difference. I never had issues with the 2 seconds that it is now, but of course other hardware and Bluetooth drivers may give different results

If you can't get it working, I'd be glad to help you out here or by email if we want to keep the PR comments relevant.

@bramd

bramd commented Nov 7, 2025

Copy link
Copy Markdown
Contributor Author

it seems like this is causing serious system test errors around NVDA exiting safely

There was a problem causing the asyncio loop to hang for 5 secs on shutdown. This has been fixed and now the tests pass as well.

@seanbudd

Copy link
Copy Markdown
Member

is this ready for re-review?

@bramd

bramd commented Nov 11, 2025

Copy link
Copy Markdown
Contributor Author

is this ready for re-review?

Almost, I finally have a working VM with no Bluetooth adapter to do some more tests (took me some time due to #19189). It seems the scanner fails silently without an adapter (e.g. just does not return any results). So I will remove some checks for Bluetooth availability that will never trigger. The BLE APIs have been added in Windows 10 creator's update, which is no longer actively supported by NVDA. So we may consider the BLE stack to be always available, even without any Bluetooth hardware in the machine.

@bramd
bramd force-pushed the dotpad-ble branch 2 times, most recently from 243346f to 8085691 Compare November 15, 2025 20:45
@bramd
bramd marked this pull request as ready for review November 15, 2025 21:34
@bramd

bramd commented Nov 19, 2025

Copy link
Copy Markdown
Contributor Author

@seanbudd Just in case you missed the status change from draft to normal PR, this is ready for re-review.

@SaschaCowley Have you been able to get this to work with your Dot Pad hardware? If not, I'd be glad to help you debug.

@seanbudd seanbudd added the merge-early Merge Early in a developer cycle label Dec 8, 2025
@seanbudd

seanbudd commented Mar 2, 2026

Copy link
Copy Markdown
Member

Hi @bramd - thanks for your work here and sorry on the delays.
It is quite challenging to review PRs this large and complex.
Could you please split this PR out into a few separate PRs.

  • The asyncioEventLoop module - please make this underscored (part of the private API) and include the tests for it
  • The tests - we are going with Test Driven Development here, so lets introduce all the tests with skip decorators on them first
  • The rest of the code

@seanbudd
seanbudd marked this pull request as draft March 2, 2026 23:19
bramd added a commit to bramd/nvda that referenced this pull request Mar 19, 2026
…round thread

Split from nvaccess#19122. Provides initialize/terminate lifecycle and runCoroutine/runCoroutineSync helpers for scheduling async work from synchronous code. Wired into core startup/shutdown. Includes 6 unit tests.
@bramd bramd mentioned this pull request Mar 19, 2026
12 tasks
bramd added a commit to bramd/nvda that referenced this pull request Mar 19, 2026
…round thread

Split from nvaccess#19122. Provides initialize/terminate lifecycle and runCoroutine/runCoroutineSync helpers for scheduling async work from synchronous code. Wired into core startup/shutdown. Includes 6 unit tests.
seanbudd pushed a commit that referenced this pull request Mar 23, 2026
N/A. This is a follow-up from #19122 (DotPad BLE support), splitting out the asyncio event loop module as requested during review.
Summary of the issue:

NVDA currently has no built-in way to run asyncio coroutines. Components that need async functionality (e.g. BLE communication) require a managed asyncio event loop running on a background thread.
Description of user facing changes:

N/A. This is a developer-facing module only.
Description of developer facing changes:

Added a private _asyncioEventLoop module that provides:

    initialize() / terminate() — lifecycle management, wired into NVDA core startup/shutdown
    runCoroutine(coro) — schedule a coroutine on the event loop (fire-and-forget)
    runCoroutineSync(coro, timeout) — schedule a coroutine and block until completion

The module is prefixed with an underscore to keep it internal to NVDA core and not part of the public add-on API.
Description of development approach:

The asyncio event loop runs on a daemon thread, started during NVDA initialization (after appModuleHandler) and terminated during shutdown (after hwIo). The terminate() function cancels all pending tasks before stopping the loop.

runCoroutineSync wraps asyncio.run_coroutine_threadsafe with optional timeout support and converts asyncio.TimeoutError to the built-in TimeoutError for a cleaner API.
bramd added a commit to bramd/nvda that referenced this pull request Mar 23, 2026
TDD approach: introduce all tests with skip decorators ahead of implementation.

- tests/unit/test_hwIo_ble.py: BLE scanner, BLE I/O, and findDeviceByAddress
- tests/unit/brailleDisplayDrivers/test_dotPad.py: buffered receive logic
- tests/unit/test_bdDetect.py: BLE device registration and matching
@bramd bramd mentioned this pull request Mar 23, 2026
12 tasks
@seanbudd

Copy link
Copy Markdown
Member

@bramd - do you think the hwIo/ble module and tests could be split out as well in a PR together?

@bramd

bramd commented Mar 24, 2026 via email

Copy link
Copy Markdown
Contributor Author

@seanbudd

Copy link
Copy Markdown
Member

I think the unit tests should be fine for testing the ble module - we will get proper manual integration testing when the rest of this PR goes in

@bramd

bramd commented Mar 25, 2026

Copy link
Copy Markdown
Contributor Author

@seanbudd Thanks, I'll try to split it up like that. Just to be sure, we will get:

  1. a PR containing hwIo.ble and related tests
  2. A PR with tests for the concrete DotPad BLE implementation
  3. A PR with the DotPad BLE implementation

Please let me know if that was what you intended. If so, I'll update/abandon #19838 and come through with the BLE module + tests first.

@seanbudd

Copy link
Copy Markdown
Member

yes that would be perfect

seanbudd pushed a commit that referenced this pull request Jun 10, 2026
Summary of the issue:

PR #19122 adds Bluetooth Low Energy (BLE) support for DotPad braille displays. As discussed in #19122, that PR is being split into smaller, easier-to-review pieces:

    PR A (this PR): the hwIo.ble module + its unit tests
    PR B: tests for the DotPad BLE driver
    PR C: bdDetect BLE integration + DotPad BLE driver implementation

This PR was previously a TDD-style "skipped tests only" PR. After feedback from @seanbudd it has been re-scoped to include the actual hwIo.ble module so the tests can be unskipped. The bdDetect-related test additions and the DotPad-specific tests have been removed; they will return in PRs C and B respectively.
Description of user facing changes:

None. hwIo.ble is a developer-facing API. End users see no changes until PRs B and C land.
Description of developer facing changes:

    New hwIo.ble submodule providing:
        Scanner — synchronous wrapper around Bleak's BleakScanner, with a deviceDiscovered extension point that fires on each advertisement and indicates whether the device is new.
        Ble — IoBase subclass for raw I/O against a BLE peripheral via a configurable read/write GATT service-and-characteristic pair, with automatic MTU chunking on writes.
        findDeviceByAddress(address, timeout, pollInterval) — helper that checks already-discovered devices and starts a scan if needed.
        Module-level scanner singleton.
    Builds on top of the existing _asyncioEventLoop module (Add private _asyncioEventLoop module #19816).
    Adds bleak==3.0.0 as a runtime dependency.
    No changes to bdDetect, no changes to any driver — those land in PR C.

Description of development approach:

Ported the hwIo.ble submodule and its unit tests from the dotpad-ble branch. The tests use unittest.mock.patch to mock bleak and runCoroutine, so they run without any BLE hardware or asyncio loop.

While unskipping, several issues with the existing tests were fixed:

    They patched runCoroutineSync, but the module uses runCoroutine. Now patching the correct symbol.
    MagicMock.__len__ was being assigned a lambda; Python looks up dunder methods on the type, not the instance, so the assignment had no effect. Now configured via __len__.return_value. (Spotted by Copilot.)
    Coroutine objects passed to mocked runCoroutine are explicitly closed in the patch's side_effect to avoid RuntimeWarning: coroutine was never awaited.
    Ble instances created in tests are tracked and closed in tearDown so the __del__ path doesn't hit a torn-down event loop.
bramd added 4 commits June 26, 2026 03:49
Replace the fragile _onReceive that did inline blocking reads with a
buffered-receive approach that accumulates bytes and extracts complete
packets. This handles resynchronization on bad sync bytes, buffer
overflow protection, and partial-packet buffering. Works with both
byte-at-a-time delivery (Serial) and packet-based delivery (BLE).

Extract _processPacket from the old inline parsing for clarity.

Add 9 unit tests covering: complete packet, byte-at-a-time, partial
chunks, multiple packets, resync, overflow, incomplete, empty, and
partial header. All pass.

Add 5 skipped BLE-specific DotPad tests (TDD skeleton for PR C):
_isBleDotPad, check(), addBleDevices registration, _tryConnect BLE.

Also fixes "Received responce" typo → "Received response".
Two correctness fixes to the buffered-receive path:

- Clear _receiveBuffer at the start of each _tryConnect probe. The buffer
  is instance-level and was reused across auto-detect probes; the USB match
  is a generic FTDI VID, so stray bytes from a non-DotPad device on an
  earlier port could prefix and corrupt the real device's first response.

- Validate the declared packet length at parse time instead of capping the
  accumulation buffer. Real DotPad frames are small, so a declared length
  beyond DP_MAX_PACKET_SIZE means a false/garbled header: discard one byte
  and resync. This drops the old pre-extraction buffer wipe, which (a)
  discarded multi-packet bursts exceeding 512 bytes before processing any
  of them and (b) could never assemble a frame larger than 512 bytes. It
  also fixes a stall where a false sync followed by a large bogus length
  waited indefinitely (recoverable only by wiping queued real packets).

Tests: replace test_bufferOverflow_cleared (obsolete semantics) with
test_implausibleLength_resynchronize, and assert extracted packet body
content in test_completePacketAtOnce to cover the header-stripping slice.
Replace the old "covered by the GNU General Public License / See the file
COPYING" reference with the current two-line license reference documented
in projectDocs/dev/copyrightHeaders.md, across the files touched in this PR.
Replace "frame" with "packet" in the comments/docstrings touched in this PR
to match the terminology used in the code (packet, packetBody, packetLength).
bramd added 2 commits July 1, 2026 22:06
- Make DP_MAX_PACKET_SIZE a docstring and add DP_MIN_PACKET_SIZE
- Resync on implausibly small declared lengths, not just oversized ones
- Use log.exception for packet-processing errors
- Hoist bdDetect import to module top in tests; clarify length-guard tests
Integrate BLE braille display detection and connection on top of the
hwIo.ble transport that now lives in master. This change contains only the
integration layer; the Bleak/hwIo.ble stack is no longer part of this branch.

- bdDetect: scan for and match BLE devices, register BLE match functions via
  DriverRegistrar.addBleDevices, and expose getBleDevicesForDriver. Guard all
  hwIo.ble.scanner access for the case where BLE is not initialized (e.g. unit
  tests), so device detection degrades gracefully instead of crashing.
- braille: thread a `ble` scan flag through detection enable/rescan, match
  detected displays by the "ble" provider, and surface individual BLE devices
  as selectable "ble:Name@Address" ports.
- DotPad driver: add check(), _isBleDotPad matcher, BLE automatic-detection
  registration, and a BLE branch in _tryConnect that opens an hwIo.ble.Ble
  device. Builds on the buffered-receive packet handling.
- Braille settings dialog: start/stop the shared BLE scanner while the display
  selection dialog is open to populate the device list.
- Enable the DotPad BLE unit tests (matcher, check, registration, _tryConnect)
  that were previously skeleton-skipped pending this integration.

Stacked on the buffered-receive branch (nvaccess#19942); retarget to master once that
merges.
seanbudd pushed a commit that referenced this pull request Jul 2, 2026
N/A. This is a follow-up to #19122 (DotPad BLE support) and #19838 (hwIo.ble module). As discussed in those PRs, the work is being split into three smaller PRs. This is "PR B" — the buffered-receive implementation and skipped BLE tests in preparation for the final BLE driver integration in PR C. There is no standalone issue tracking this change.
Summary of the issue:

The DotPad driver's _onReceive method reads remaining packet bytes synchronously via self._dev.read() and raises RuntimeError on any malformed byte, which can kill the display connection. This approach also cannot handle BLE's packet-based delivery model where entire packets arrive in a single callback. The buffered-receive replaces this with a more robust approach as a prerequisite for BLE support.
Description of user facing changes:

DotPad braille displays now handle malformed serial data more gracefully. Instead of crashing the display connection on a single corrupted byte, the driver resynchronizes and continues operating. In practice, since DotPad devices currently only connect over USB serial (which is a stable connection), users are unlikely to notice this change. The real benefit comes when BLE support lands in PR C, where packet-based delivery makes the buffered approach essential.
Description of developer facing changes:

    Replaced _onReceive(self, header1: bytes) with a buffered-receive approach _onReceive(self, data: bytes) that accepts variable-length input (1 byte from Serial, full packets from BLE).
    Extracted _processPacket(self, packetBody: bytes) from the old inline parsing for clarity.
    Added DP_MAX_PACKET_SIZE protocol constant to defs.py.
    The buffered-receive handles: resynchronization on bad sync bytes, buffer overflow protection, partial-packet buffering, and multiple packets in a single call.

Description of development approach:

Ported the buffered-receive logic from the dotpad-ble branch (#19122), keeping only the transport-agnostic parts. No BLE-specific code is included — that lands in PR C.

The approach replaces synchronous self._dev.read() calls within the callback with a _receiveBuffer that accumulates bytes across callbacks. Complete packets are extracted from the buffer as they become available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. merge-early Merge Early in a developer cycle

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to connect Dot Pad via Bluetooth

5 participants