Skip to main content

Overview

Braille display drivers enable NVDA to communicate with braille displays, allowing blind users to read screen content in braille. Each driver handles the specific protocol and hardware interface for a particular manufacturer’s displays.

Driver Architecture

Base Class

All braille drivers inherit from braille.BrailleDisplayDriver. This provides the core framework for:
  • Device detection and connection
  • Braille cell rendering
  • Input handling (keys, routing buttons, wheels)
  • Configuration management

Key Components

  1. Device Communication: Uses hwIo module for hardware I/O operations
  2. Protocol Implementation: Packet-based communication with displays
  3. Input Mapping: Translates hardware inputs to NVDA gestures
  4. Cell Translation: Converts Unicode braille to device-specific formats

Creating a Basic Driver

Minimal Driver Structure

Here’s the simplest possible braille driver:
brailleDisplayDrivers/noBraille.py
import braille

class BrailleDisplayDriver(braille.BrailleDisplayDriver):
    """A dummy braille display driver used to disable braille in NVDA."""
    
    name = "noBraille"
    # Translators: Is used to indicate that braille support will be disabled.
    description = _("No braille")
    
    @classmethod
    def check(cls):
        return True

Required Attributes

name
str
required
Unique identifier for the driver (should match the module filename)
description
str
required
Human-readable name shown in NVDA’s braille display settings
isThreadSafe
bool
default:"False"
Whether the driver can be used safely from background threads
supportsAutomaticDetection
bool
default:"False"
Whether the driver supports automatic USB/Bluetooth detection

Full Driver Implementation

Freedom Scientific Example

The Freedom Scientific driver demonstrates a complete implementation:
import braille
import inputCore
import hwIo
from hwIo import intToByte
import serial
import bdDetect

BAUD_RATE = 57600
PARITY = serial.PARITY_NONE

MODELS = {
    "Focus 14": 14,
    "Focus 40": 40,
    "Focus 80": 80,
}

class BrailleDisplayDriver(braille.BrailleDisplayDriver):
    name = "freedomScientific"
    description = _("Freedom Scientific Focus/PAC Mate series")
    isThreadSafe = True
    supportsAutomaticDetection = True
    receivesAckPackets = True
    timeout = 0.2
    
    @classmethod
    def registerAutomaticDetection(cls, driverRegistrar):
        """Register USB/Bluetooth IDs for automatic detection."""
        driverRegistrar.addUsbDevices(bdDetect.DeviceType.USB, {
            "VID_0F4E&PID_0100",  # Focus 1
            "VID_0F4E&PID_0111",  # Focus 2
            "VID_0F4E&PID_0112",  # Focus Blue
        })

Device Initialization

def __init__(self, port="auto"):
    super().__init__()
    self.numCells = 0
    self._model = None
    
    # Connect to device
    for match in self._getAutoPorts(usb=True, bluetooth=True):
        try:
            self._dev = hwIo.Serial(
                match["port"],
                baudrate=BAUD_RATE,
                parity=PARITY,
                timeout=self.timeout,
                writeTimeout=self.timeout,
                onReceive=self._onReceive,
            )
        except Exception:
            continue
        
        # Query device info
        self._sendPacket(FS_PKT_QUERY)
        
        # Wait for response with device info
        for _i in range(3):
            self._dev.waitForRead(self.timeout)
            if self.numCells:
                break
        
        if self.numCells:
            log.info(f"Found {self._model} with {self.numCells} cells")
            break
    else:
        raise RuntimeError("No display found")

Protocol Implementation

Packet Structure

Most braille displays use packet-based protocols:
# Packet types
FS_PKT_QUERY = b"\x00"      # Query device info
FS_PKT_ACK = b"\x01"        # Acknowledgment
FS_PKT_KEY = b"\x03"        # Key press/release
FS_PKT_BUTTON = b"\x04"     # Routing button
FS_PKT_WHEEL = b"\x05"      # Wheel turn
FS_PKT_WRITE = b"\x81"      # Write cells
FS_PKT_EXT_KEY = b"\x82"    # Extended keys

def _sendPacket(self, packetType, data=b""):
    """Send a packet to the display."""
    packet = packetType + intToByte(len(data)) + data
    self._dev.write(packet)

def _onReceive(self, data):
    """Handle incoming data from the display."""
    if not data:
        return
    
    packetType = data[0:1]
    payload = data[2:]
    
    if packetType == FS_PKT_KEY:
        self._handleKeyPacket(payload)
    elif packetType == FS_PKT_BUTTON:
        self._handleButtonPacket(payload)
    elif packetType == FS_PKT_INFO:
        self._handleInfoPacket(payload)

Writing to Display

def display(self, cells):
    """Display braille cells on the display.
    
    @param cells: List of cell values (0-255) in Unicode braille format
    """
    # Translate cells if needed
    if self._useTranslationTable:
        cells = self._translate(cells, FOCUS_1_TRANSLATION_TABLE)
    
    # Send to display
    data = bytes(cells)
    self._sendPacket(FS_PKT_WRITE, data)

Cell Translation

Some displays use non-standard dot mappings:
def _makeTranslationTable(dotsTable):
    """Create a translation table for braille dot combinations.
    
    @param dotsTable: List of 8 bitmasks for each dot (dot 1-8)
    @return: Translation table with 256 entries
    """
    def isoDot(number):
        """Returns ISO 11548-1 formatted braille dot."""
        return 1 << (number - 1)
    
    outputTable = [0] * 256
    for byte in range(256):
        cell = 0
        for dot in range(8):
            if byte & isoDot(dot + 1):
                cell |= dotsTable[dot]
        outputTable[byte] = cell
    return outputTable

# Example: Focus 1 uses different dot ordering
FOCUS_1_DOTS_TABLE = [
    0x01, 0x02, 0x04, 0x10,
    0x20, 0x40, 0x08, 0x80,
]
FOCUS_1_TRANSLATION_TABLE = _makeTranslationTable(FOCUS_1_DOTS_TABLE)

Input Handling

Key Gestures

from inputCore import InputGesture

class InputGesture(InputGesture):
    source = "freedomScientific.braille"
    
    def __init__(self, keys=None, routing=None):
        super().__init__()
        self.keys = keys
        self.routingIndex = routing
        self.id = self._makeId()
    
    def _makeId(self):
        if self.routingIndex is not None:
            return f"routing{self.routingIndex}"
        elif self.keys:
            return "+".join(self.keys)
        return "unknown"

def _handleKeyPacket(self, payload):
    """Process key press/release events."""
    keys = self._parseKeys(payload)
    if keys:
        gesture = InputGesture(keys=keys)
        try:
            inputCore.manager.executeGesture(gesture)
        except inputCore.NoInputGestureAction:
            pass

def _handleButtonPacket(self, payload):
    """Process routing button presses."""
    routingIndex = payload[0]
    gesture = InputGesture(routing=routingIndex)
    try:
        inputCore.manager.executeGesture(gesture)
    except inputCore.NoInputGestureAction:
        pass

Gesture Mapping

gestureMap = inputCore.GlobalGestureMap({
    "globalCommands.GlobalCommands": {
        "braille_scrollBack": ("br(freedomScientific):leftAdvanceBar",),
        "braille_scrollForward": ("br(freedomScientific):rightAdvanceBar",),
        "braille_previousLine": ("br(freedomScientific):leftRockerBarUp",),
        "braille_nextLine": ("br(freedomScientific):leftRockerBarDown",),
        "braille_routeTo": ("br(freedomScientific):routing",),
    },
})

Automatic Detection

USB Detection

@classmethod
def registerAutomaticDetection(cls, driverRegistrar: bdDetect.DriverRegistrar):
    """Register device IDs for automatic detection."""
    # USB devices
    driverRegistrar.addUsbDevices(bdDetect.DeviceType.USB, {
        "VID_0F4E&PID_0100",  # Focus 1
        "VID_0F4E&PID_0111",  # Focus 2
        "VID_0F4E&PID_0112",  # Focus Blue
    })
    
    # Bluetooth devices
    driverRegistrar.addBluetoothDevices(lambda m: 
        m.type == bdDetect.DeviceType.SERIAL and
        m.id.startswith("Bluetooth_") and
        ("Focus" in m.deviceInfo.get("friendlyName", ""))
    )

Advanced Features

Status Cells

Some displays have dedicated status cells:
def display(self, cells):
    """Display cells with status cells support."""
    # Focus 1 has 3 status cells on each side
    if self.numCells in FOCUS_1_CELL_COUNTS:
        # Pad with status cells
        statusCells = [0] * 3  # Left status
        statusCells += [0]      # Separator
        statusCells += cells    # Main content
        statusCells += [0]      # Separator
        statusCells += [0] * 3  # Right status
        cells = statusCells
    
    self._sendCells(cells)

Configuration Settings

from autoSettingsUtils.driverSetting import BooleanDriverSetting

class BrailleDisplayDriver(braille.BrailleDisplayDriver):
    # ...
    
    supportedSettings = [
        BooleanDriverSetting(
            "wordWrap",
            _("Word wrap"),
            defaultVal=True,
        ),
    ]
    
    def _get_wordWrap(self):
        return self._wordWrap
    
    def _set_wordWrap(self, value):
        self._wordWrap = value
        # Update display behavior

Testing Your Driver

Debug Logging

from logHandler import log

class BrailleDisplayDriver(braille.BrailleDisplayDriver):
    # ...
    
    def _onReceive(self, data):
        log.debug(f"Received {len(data)} bytes: {data.hex()}")
        # Process data...
    
    def display(self, cells):
        log.debug(f"Displaying {len(cells)} cells")
        # Send to display...

Manual Testing

1

Install the driver

Place your driver file in source/brailleDisplayDrivers/yourdriver.py
2

Restart NVDA

Restart NVDA or reload plugins (NVDA+Control+F3)
3

Select your driver

Open NVDA Settings > Braille and select your driver from the list
4

Test functionality

  • Verify cell output displays correctly
  • Test all buttons and gestures
  • Check routing buttons work
  • Verify automatic detection (if supported)

Best Practices

Thread Safety

Mark isThreadSafe = True if your driver uses proper locking and can handle concurrent calls

Error Handling

Always handle connection errors gracefully and clean up resources in terminate()

Timeout Management

Use appropriate timeouts to prevent NVDA from freezing if the device becomes unresponsive

Translation Tables

Cache translation tables - don’t regenerate them for every cell update
Always test your driver with the actual hardware. Emulators may not accurately represent device behavior.

Common Issues

Device Not Detected

  • Verify USB/Bluetooth IDs are correct
  • Check device drivers are installed
  • Ensure registerAutomaticDetection() is implemented
  • Test with manual port selection first

Garbled Braille Output

  • Check if device uses non-standard dot mapping
  • Verify cell translation table is correct
  • Ensure byte order matches device expectations

Input Not Working

  • Verify packet parsing is correct
  • Check gesture IDs match NVDA’s expectations
  • Ensure InputGesture.source is set correctly
  • Test with NVDA input logging enabled

Reference

Key Modules

  • braille: Base braille display driver framework
  • hwIo: Hardware I/O for serial, USB, HID communication
  • bdDetect: Braille display automatic detection
  • inputCore: Input gesture handling
  • brailleInput: Braille keyboard input support

Example Drivers

Study these drivers in source/brailleDisplayDrivers/ for reference:
  • freedomScientific.py: Full-featured driver with complex protocol
  • alva.py: Driver with automatic detection
  • noBraille.py: Minimal driver structure
  • hidBrailleStandard.py: HID-based braille standard protocol

Developer Guide

See the NVDA Developer Guide for more information on plugin development

Build docs developers (and LLMs) love