diff --git a/src/fenrirscreenreader/brailleDriver/brlapiDriver.py b/src/fenrirscreenreader/brailleDriver/brlapiDriver.py index 4b91b01b..f2df2a50 100644 --- a/src/fenrirscreenreader/brailleDriver/brlapiDriver.py +++ b/src/fenrirscreenreader/brailleDriver/brlapiDriver.py @@ -2,65 +2,167 @@ # -*- coding: utf-8 -*- # Fenrir TTY screen reader -# By Chrys, Storm Dragon, and contributers. +# By Chrys, Storm Dragon, and contributors. from fenrirscreenreader.core import debug from fenrirscreenreader.core.brailleDriver import brailleDriver class driver(brailleDriver): + """BRLAPI implementation of the braille driver interface. + Supports all braille displays compatible with BRLTTY.""" + def __init__(self): + """Initialize the BRLAPI driver.""" brailleDriver.__init__(self) self._brl = None + self._last_written_text = "" def initialize(self, environment): + """Initialize the braille driver with BRLAPI connection.""" self.env = environment try: import brlapi self._brl = brlapi.Connection() self._deviceSize = self._brl.displaySize + # Accept all key types from any braille display + self._brl.acceptKeys(brlapi.rangeType_all, [0, brlapi.KEY_MAX]) + self._current_cursor_pos = 0 except Exception as e: - print(e) - self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR) + self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR) return self._isInitialized = True def getDeviceSize(self): - if not self._isInitialized: - return (0,0) - if not self._deviceSize: - return (0,0) + """Get the size of the connected braille display.""" + if not self._isInitialized or not self._deviceSize: + return (0, 0) return self._deviceSize - def flush(self): + def writeText(self, text): + """Write text to the braille display.""" if not self._isInitialized: return try: - self._brl.writeText('',0) + self._last_written_text = text + # Handle unicode properly for international braille + if isinstance(text, str): + self._brl.writeText(text) + else: + self._brl.writeText(text.decode('utf-8', 'replace')) except Exception as e: - self.env['runtime']['debug'].writeDebugOut('BRAILLE.flush '+str(e),debug.debugLevel.ERROR) - - def writeText(self,text): - if not self._isInitialized: - return - try: - self._brl.writeText(text) - except Exception as e: - self.env['runtime']['debug'].writeDebugOut('BRAILLE.writeText '+str(e),debug.debugLevel.ERROR) + self.env['runtime']['debug'].writeDebugOut('BRAILLE.writeText ' + str(e), debug.debugLevel.ERROR) def connectDevice(self): - self._brl = brlapi.Connection() + """Establish connection with BRLAPI.""" + try: + import brlapi + self._brl = brlapi.Connection() + # Accept all key types from the connected display + self._brl.acceptKeys(brlapi.rangeType_all, [0, brlapi.KEY_MAX]) + return True + except Exception as e: + self.env['runtime']['debug'].writeDebugOut('BRAILLE.connectDevice ' + str(e), debug.debugLevel.ERROR) + return False def enterScreen(self, screen): + """Enter a specific TTY mode.""" if not self._isInitialized: return - self._brl.enterTtyMode(int(screen)) + try: + self._brl.enterTtyMode(int(screen)) + except Exception as e: + self.env['runtime']['debug'].writeDebugOut('BRAILLE.enterScreen ' + str(e), debug.debugLevel.ERROR) - def leveScreen(self): + def leaveScreen(self): + """Leave the current TTY mode.""" if not self._isInitialized: return - self._brl.leaveTtyMode() + try: + self._brl.leaveTtyMode() + except Exception as e: + self.env['runtime']['debug'].writeDebugOut('BRAILLE.leaveScreen ' + str(e), debug.debugLevel.ERROR) + + def setCursor(self, position): + """Set the cursor position on the braille display.""" + if not self._isInitialized: + return + try: + max_pos = self.getDeviceSize()[0] - 1 + self._current_cursor_pos = max(0, min(position, max_pos)) + # Use BRLAPI's cursor command which works across different displays + self._brl.writeDots(1 << self._current_cursor_pos) + except Exception as e: + self.env['runtime']['debug'].writeDebugOut('BRAILLE.setCursor ' + str(e), debug.debugLevel.ERROR) + + def getCursorPosition(self): + """Get the current cursor position.""" + if not self._isInitialized: + return 0 + return self._current_cursor_pos + + def clear(self): + """Clear the braille display.""" + if not self._isInitialized: + return + try: + self._brl.writeText('') + self._last_written_text = "" + self._current_cursor_pos = 0 + except Exception as e: + self.env['runtime']['debug'].writeDebugOut('BRAILLE.clear ' + str(e), debug.debugLevel.ERROR) + + def flush(self): + """Force an update of the physical display.""" + if not self._isInitialized: + return + try: + if self._last_written_text: + self._brl.writeText(self._last_written_text) + except Exception as e: + self.env['runtime']['debug'].writeDebugOut('BRAILLE.flush ' + str(e), debug.debugLevel.ERROR) + + def handleKeyInput(self, key): + """Handle input from any BRLTTY-compatible braille display. + Returns the key event for processing by Fenrir's input system.""" + if not self._isInitialized: + return False + try: + key_code = self._brl.readKey(wait=0) + if not key_code: + return False + + # Let Fenrir's input system handle the key + # This allows proper handling regardless of display type + if self.env['runtime']['debug'].debugLevel >= debug.debugLevel.INFO: + self.env['runtime']['debug'].writeDebugOut( + 'BRAILLE.keyPressed: ' + str(key_code), + debug.debugLevel.INFO + ) + return True + except brlapi.ConnectionError: + self.env['runtime']['debug'].writeDebugOut( + 'BRAILLE: Connection lost. Attempting to reconnect...', + debug.debugLevel.ERROR + ) + self.connectDevice() + return False + except Exception as e: + self.env['runtime']['debug'].writeDebugOut( + 'BRAILLE.handleKeyInput ' + str(e), + debug.debugLevel.ERROR + ) + return False def shutdown(self): + """Shut down the BRLAPI driver.""" if not self._isInitialized: return - self.leveScreen() + try: + self.clear() + self.leaveScreen() + if self._brl: + self._brl.closeConnection() + self._brl = None + self._isInitialized = False + except Exception as e: + self.env['runtime']['debug'].writeDebugOut('BRAILLE.shutdown ' + str(e), debug.debugLevel.ERROR) diff --git a/src/fenrirscreenreader/core/brailleDriver.py b/src/fenrirscreenreader/core/brailleDriver.py index 8ed4c66c..bdd6f3e3 100644 --- a/src/fenrirscreenreader/core/brailleDriver.py +++ b/src/fenrirscreenreader/core/brailleDriver.py @@ -1,82 +1,156 @@ -#!/usr/bin/python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- + # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. -import brlapi +from abc import ABC, abstractmethod from fenrirscreenreader.core import debug -class brailleDriver(): +class brailleDriver(ABC): + """Base class for Fenrir braille display drivers. + + This abstract base class defines the interface that all braille drivers must implement. + It provides basic initialization state tracking and defines the required methods + for interacting with braille displays. + """ + def __init__(self): + """Initialize the driver with default state.""" self._isInitialized = False - self._brl = None self.deviceSize = None - + self.env = None + self._current_cursor_pos = 0 + self._display_content = "" + + @abstractmethod def initialize(self, environment): - """Initialize the BRLTTY connection.""" + """Initialize the braille driver with the given environment. + + Args: + environment (dict): The Fenrir environment dictionary containing runtime settings + and helper objects. + """ self.env = environment - try: - self._brl = brlapi.Connection() - self._brl.enterTtyMode() - self.deviceSize = self._brl.displaySize - self._isInitialized = True - return True - except Exception as e: - self.env['runtime']['debug'].writeDebugOut('ERROR: Initializing braille failed:' + str(e),debug.debugLevel.ERROR) - return False + self._isInitialized = True + @abstractmethod def getDeviceSize(self): - """Get the size of the braille display.""" + """Get the size of the connected braille display. + + Returns: + tuple: A (columns, rows) tuple indicating the display dimensions. + Returns (0, 0) if no display is connected or not initialized. + """ if not self._isInitialized: return (0, 0) - return self.deviceSize if self.deviceSize else (0, 0) + return (0, 0) + @abstractmethod def writeText(self, text): - """Write text to the braille display.""" + """Write text to the braille display. + + Args: + text (str): The text to be written to the display. + """ if not self._isInitialized: return - try: - self._brl.writeText(text) - except Exception as e: - self.env['runtime']['debug'].writeDebugOut('ERROR: Writing braille failed:' + str(e),debug.debugLevel.ERROR) - def readKeypress(self): - """Read a keypress from the braille display.""" - if not self._isInitialized: - return None - try: - return self._brl.readKey(wait=0) - except Exception as e: - self.env['runtime']['debug'].writeDebugOut('ERROR: Reading key failed:' + str(e),debug.debugLevel.ERROR) - return None + @abstractmethod + def connectDevice(self): + """Establish connection with the braille display. + + This method should handle the initial connection setup with the display. + It should be called before any other display operations. + """ + pass + @abstractmethod def enterScreen(self, screen): - """Enter a new screen context.""" + """Enter a specific screen/TTY mode. + + Args: + screen (int): The screen/TTY number to enter. + """ if not self._isInitialized: return - try: - self._brl.enterTtyModeWithPath(screen) - except Exception as e: - self.env['runtime']['debug'].writeDebugOut('ERROR: Entering screen failed:' + str(e),debug.debugLevel.ERROR) + @abstractmethod def leaveScreen(self): - """Leave the current screen context.""" + """Leave the current screen/TTY mode. + + This method should clean up any screen-specific state and + prepare for potential screen switches. + """ + if not self._isInitialized: + return + + @abstractmethod + def setCursor(self, position): + """Set the cursor position on the braille display. + + Args: + position (int): The position where the cursor should be placed. + """ + if not self._isInitialized: + return + self._current_cursor_pos = max(0, min(position, self.getDeviceSize()[0] - 1)) + + @abstractmethod + def getCursorPosition(self): + """Get the current cursor position on the braille display. + + Returns: + int: The current cursor position. + """ + if not self._isInitialized: + return 0 + return self._current_cursor_pos + + @abstractmethod + def clear(self): + """Clear the braille display.""" + if not self._isInitialized: + return + self._display_content = "" + self._current_cursor_pos = 0 + + @abstractmethod + def flush(self): + """Force an update of the physical display with current content.""" if not self._isInitialized: return - try: - self._brl.leaveTtyMode() - except Exception as e: - self.env['runtime']['debug'].writeDebugOut('ERROR: Leaving screen failed:' + str(e),debug.debugLevel.ERROR) def shutdown(self): - """Shutdown the braille driver.""" + """Shut down the braille driver and clean up resources. + + This method ensures proper cleanup of resources and + disconnection from the braille display. + """ if not self._isInitialized: return - try: - self.leaveScreen() - if self._brl: - self._brl.closeConnection() - self._brl = None - self._isInitialized = False - except Exception as e: - self.env['runtime']['debug'].writeDebugOut('ERROR: Shutting down braille failed:' + str(e),debug.debugLevel.ERROR) + self.clear() + self.leaveScreen() + self._isInitialized = False + + @property + def is_initialized(self): + """Check if the driver is initialized. + + Returns: + bool: True if the driver is initialized, False otherwise. + """ + return self._isInitialized + + @abstractmethod + def handleKeyInput(self, key): + """Handle input from the braille display's keys. + + Args: + key: The key event from the braille display. + Returns: + bool: True if the key was handled, False otherwise. + """ + if not self._isInitialized: + return False + return False