<gh_stars>1-10
# Copyright (c) 2017 <NAME>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


'''
  This python module greatly extends the functionality of
  16x1 or 16x2 dot character LCD displays, using the HD44780
  controller or one alike, as well as it provides an easy way
  to interact with it. Briefly you can:
    * Display text on the LCD screen (duh)
    * Split the text into unlimited lines and lines into unlimited cells
    * Format cell widths using tab stops
    * Scroll through all of the contents with ease and in many different ways
    * Load extended dot patterns and draw them on the screen (coming soon)


  Author and Maintainer
  <NAME> <<EMAIL>>
'''


# Constants and Flags
# -------------------

# Current module version
VERSION = '0.7.1'

# Defines the number of lines on the LCD screen
LCD_LINES = 2

# Defines the number of characters per line
CHARS_PER_LINE = 16

# Flag to make cursor visible via the `cursor` function
CURSOR_VISIBLE = 0x01

# Flag to make cursor blink via the `cursor` function
CURSOR_BLINK = 0x02


# _Buffer is a helper class to control the module's
# internal text buffer. It parses the given text to lines
# ad holds an internal pointer to the current line
class _Buffer:

  # Parses the raw string internally and constructs the lines array
  # If 'string' is None, then it clears the buffer's contents
  def parse(self, string):
    self._lines = []
    self._raw = string

    if self._raw:
      for line in self._raw.split('\n'):
        self._lines += [ _Line(self).parse(line) ]
      
    self._reset()
    return self


  # Appends more lines at the end of the buffer
  def append(self, string):
    return self.parse(self._raw + '\n' + string)

  
  # Clears the buffer contents
  def clear(self):
    return self.parse(None)


  # Returns the _Line of text at index
  # If index is left blank, _current_line_index is used
  # If index exceeds line_count(), it returns None
  def line(self, index=None):
    if self._lines:
      index = self._current_line_index if index == None else index
      #return self._lines[ index % self.line_count() ]
      if index < self.line_count():
        return self._lines[ index ]
    
    return None


  # Returns the number of lines in the buffer
  def line_count(self):
    return len(self._lines)


  # Returns True if the given _Line is currently displayed on the LCD
  # Returns False otherwise
  def is_displayed(self, line):
    index = self._lines.index(line)
    if index is not None and index in range(self._current_line_index,\
                                self._current_line_index + LCD_LINES):
      return True
    else: return False


  # Returns True if contents can scroll to 'position', False otherwise
  # In other words True is returned if 'position' is within [0, line_count())
  def scrolls_to(self, position):
    return position in range(self.line_count())


  # Scrolls to the line at 'position'
  # If 'position' is negative it scrolls to the first line, whereas
  # if 'position' is equal or greater line_count(), it scrolls to the last
  # _Line
  def scroll_to(self, position):
    if self.scrolls_to(position):
      self._current_line_index = position
    else:
      self.scroll_top() if position < 0 else self.scroll_bottom()


  # Scrolls the contents down by 'offset' lines
  # Returns True if contents will further scroll to that direction by the same
  # amount, False otherwise
  def scroll_down(self, offset=1):
    self.scroll_to( self._current_line_index + offset )
    return self.scrolls_to( self._current_line_index + offset )


  # Scrolls the contents up by 'offset' lines
  # Returns True if contents will further scroll to that direction by the same
  # amount, False otherwise
  def scroll_up(self, offset=1):
    self.scroll_to( self._current_line_index - offset )
    return self.scrolls_to( self._current_line_index - offset )


  # Scrolls the contents to the first line in the buffer
  def scroll_top(self):
    self.scroll_to(0)


  # Scrolls the contents to the last line in the buffer
  def scroll_bottom(self):
    self.scroll_to(self.line_count() - 1)


  # Concatenates all the lines in the buffer into one formatted string
  # Mainly to be called from __str__() so users are able to print the
  # buffer on the computer without any effort
  def _format_contents(self):
    if self._lines:
      formatted = ""

      for i in range(LCD_LINES):
        formatted += str(self[ self._current_line_index + i ]) + '\n'

      return formatted


  # Resets the internal line index
  def _reset(self):
    self._current_line_index = 0


  # Initializes the _Buffer
  def __init__(self):
    self._lines = []
    self._current_line_index = 0

  
  # Uses _format_contents to return a formatted string of its contents
  # Falls back to its name
  def __str__(self):
    return self._format_contents() or _Buffer.__name__


  # Overloads operator.len() to return the _Buffer's line_count()
  def __len__(self):
    return self.line_count()


  # Overloads operator [] to access a line in the buffer
  def __getitem__(self, index):
    return self.line(index)


  # Returns the internal _lines array when involved in for...in loops
  def __iter__(self):
    return iter(self._lines)


# Class _Buffer END
################################################################################


# Helper class that formats a given line of string for display
# on the lcd screen
# The text in the line can be split into multiple cells using tab stops
# Each cell is displayed and scrolled seperately on the screen
class _Line:

  # Parses the given string
  def parse(self, string):
    self._cells = []
    self._raw = string

    if self._raw:
      cells = self._raw.split('\t')

      for cell in cells:
        self._cells += [ _Cell(self, text=cell) ]
      
      self._auto_tab_stops()
    
    self._reset()
    return self


  # Returns the text contained in cell at index
  # If index is left blank, the internal cell index is used
  # If index exceeds cell_count(), it circles back to the beginning
  def cell(self, index=None):
    if self._cells:
      index = self._current_cell_index if index == None else index
      # return self._cells[ index % self.cell_count() ]
      if index < self.cell_count():
        return self._cells[ index ]
    
    return None


  # Returns the number of cells in the last parsed line of text
  # That typically corresponds to the number of '\t' characters
  # in the line plus 1
  def cell_count(self):
    return len(self._cells)


  # Lets the user set custom tab stops
  # Tab stops are used to control the number and width of cells
  # in a line of text. These cells will be displayed and scrolled
  # independently. You can change between cells using the '\t'
  # character when you provide the string to be written on the LCD screen
  def set_tab_stops(self, tab_stops):
    if len(tab_stops) < (len(self._cells) - 1):
      raise ValueError("Fewer tab stops given than tab characters in line")
    if not all(x < y for x, y in zip(tab_stops, tab_stops[1:])):
      raise ValueError("Tab stops must be in ascending order")
 
    self._tab_stops = tab_stops
    self._distribute_cell_widths()


  # Returns True if this _Line is currently displayed on the LCD screen
  # Returns False otherwise
  def is_displayed(self, cell=None):
    if cell:
      index = self._cells.index(cell)
    
      if self.is_displayed() and index in range(self._current_cell_index,\
                   self._current_cell_index + self._last_visible_cell_index):
        return True
      else: return False

    else: return self._parent.is_displayed(self)


  # Returns True if contents can scroll to 'position', False otherwise
  # In other words True is returned if 'position' is within [0, cell_count())
  def scrolls_to(self, position):
    return position in range(self.cell_count())


  # Scrolls to the cell at 'position'
  # If 'position' is negative it scrolls to the first cell, whereas
  # if 'position' is equal or grater than cell_count(), it scrolls to the last
  # cell
  def scroll_to(self, position):
    if self.scrolls_to(position):
      self._current_cell_index = position
    else:
      self.scroll_start() if position < 0 else self.scroll_end()


  # Scrolls the contents to the left by 'offset' cells
  def scroll_left(self, offset=1):
    self.scroll_to( self._current_cell_index + offset )
    return self.scrolls_to( self._current_cell_index + offset )


  # Scrolls the contents to the right by 'offset' cells
  def scroll_right(self, offset=1):
    self.scroll_to( self._current_cell_index - offset )
    return self.scrolls_to( self._current_cell_index - offset )


  # Scrolls to the start of the line
  def scroll_start(self):
    self.scroll_to(0)


  # Scrolls to the end of the line
  def scroll_end(self):
    self.scroll_to(self.cell_count() - 1)


  # Turns its contents into a formatted string to be written on the LCD screen
  def _format_contents(self, tab_stops=None):
    formatted = ""

    if self._cells:
      for cell in self._cells[self._current_cell_index:]:
        if len(formatted) >= CHARS_PER_LINE: break
        formatted += str(cell)
        self._last_visible_cell_index = self._cells.index(cell)

      return formatted[:CHARS_PER_LINE]


  # Resets the internal cell index
  def _reset(self):
    self._current_cell_index = 0
    self._last_visible_cell_index = 0


  # Generates evenly spaced tab stops based on their count
  # Up to 16 tab stops can be generated automatically and fitted
  # within the 16-character line of the lcd screen. If their width
  # cannot be divided exactly to an integer amount, it will be floored
  # to the nearest integer, so contents of the last cell might be
  # partly or totally hidden, and you should call scroll_left() on the
  # line to make them visible
  def _auto_tab_stops(self):
    count = min(self.cell_count(), 16)
    tab_stops = [] 
    for i in range(count - 1):
      tab_stops += [(16 / count) * (i + 1)]

    self.set_tab_stops(tab_stops)
  
  
  # Distributes the width of each cell in the line
  def _distribute_cell_widths(self):
    for cell in self._cells:
      index = self._cells.index(cell)
      cell.set_width( self._calculate_cell_width(index) )


  # Calculates the width of a cell based on its bounding tab stops
  # If index is left blank, _current_cell_index will be used.
  # Notice that this function does not check if index is out of bounds
  def _calculate_cell_width(self, index):
    last = ([0] + self._tab_stops)[-1]
    padding_end = 16 - (last % 16)
    cell_start = ([0] + self._tab_stops)[index]
    cell_end   = (self._tab_stops + [last + padding_end])[index]
    return cell_end - cell_start
     

  # Initializes the _Line
  def __init__(self, parent):
    if not isinstance(parent, _Buffer):
      raise TypeError("Parent should be an instance of _Buffer")
    self._parent = parent
    self.parse(None)


  # Returns the formatted contents of the line that are to be written
  # on the lcd screen, or __name__ if no line is yet parsed
  def __str__(self):
    return self._format_contents() or _Line.__name__


  # Returns the _Line's cumulative sum of its cell widths
  def __len__(self):
    length = 0
    for cell in self._cells:
      length += cell.get_width()
    return length


  # Makes it possible to access a _Cell by applying indexing on the _Line
  def __getitem__(self, index):
    return self.cell(index)


  # The next two methods implement the iterator protocol in
  # order to make instaces of this class iterable. Users can
  # therefore iterate between cells in a line easily in a
  # for...in loop
  def __iter__(self):
    return iter(self._cells)


# Class _Line END
################################################################################


class _Cell:

  # Sets the width of the cell
  def set_width(self, width):
    if width < 1 or width > CHARS_PER_LINE:
      raise ValueError("Width of cell should be between 1 and {} characters"\
                                                      .format(CHARS_PER_LINE))

    self._width = width
    self.scroll_to(0)


  # Returns the width of the cell
  def get_width(self):
    return self._width


  # Returns true if the _Cell is currently displayed on the LCD screen
  # Returns False otherwise
  def is_displayed(self):
    return self._parent.is_displayed(self)


  # Returns True if contents can scroll to 'position', False otherwise
  # Contents can scroll until there are no more hidden characters past the
  # right boundary of the cell
  def scrolls_to(self, position):
    return position in range(len(self.text) - self.get_width() + 1)


  # Scrolls the text to the character at 'position' within the displayed string
  # If 'position' is zero or negative, it scrolls to the top, whereas if
  # 'position' causes the last character in the text to be displayed before the
  # right boundary of the cell (displaces the text too much), it scrolls to the
  # end
  def scroll_to(self, position):
    if self.scrolls_to(position):
      self._scroll_offset = position
    elif len(self.text) > self.get_width():
      self.scroll_start() if position < 0 else self.scroll_end()


  # Scrolls the contents to the left by 'offset' characters
  def scroll_left(self, offset=1):
    self.scroll_to( self._scroll_offset + offset )
    return self.scrolls_to( self._scroll_offset + offset )


  # Scrolls the contents to the right by 'offset' characters
  def scroll_right(self, offset=1):
    self.scroll_to( self._scroll_offset - offset )
    return self.scrolls_to( self._scroll_offset - offset )


  # Scrolls to the start of the text
  def scroll_start(self):
    self.scroll_to(0)


  # Scrolls to the end of the text
  def scroll_end(self):
    self.scroll_to(len(self.text) - self.get_width())


  # Returns the portion of text that fits inside the cell. If the
  # text length is larger than the cell's width, it can be scrolled
  # using scroll_right(), scroll_left() or scroll_to() methods
  def _format_contents(self):
    start = self._scroll_offset
    end = start + self.get_width()
    return self.text[ start : end ].ljust(self.get_width())


  # Initializes the cell with some text and specifies its width
  # (16 characters by default)
  def __init__(self, parent, text, width=16):
    if not isinstance(parent, _Line):
      raise TypeError("Parent must be an instance of _Line")
    self._parent = parent
    self.text = text
    self.set_width(width)
    self._scroll_offset = 0


  def __str__(self):
    return self._format_contents() or _Cell.__name__


# Class _Cell END
################################################################################


from abc import ABCMeta, abstractmethod
from threading import Timer


# _Scroller is a base class that serves as a common ancestor of the concrete
# implementation of all the _Scroller classes. It provides the common interface
# used by _ScrollerBuilder
class _Scroller:

  # Make this an abstract class 
  __metaclass__ = ABCMeta


  # Performs the scrolling once
  def once(self):
    self._perform_scroll()


  # Performs the scrolling repeatedly every 'seconds'
  # This method starts a new Timer that calls the same function after the given
  # time delay in seconds, until the contents of the container can no longer
  # scroll. If circular is set to True, the scrolling is going to repeat back
  # and forth forever until stop() is called
  def every(self, seconds, callback=None):
    if not self._stopped:
      if self._perform_scroll():
        Timer(seconds, self.every, [seconds, callback]).start()
      elif callback: Timer(seconds, callback).start()
    else: self._stopped = False

    return self

  # Scrolls indefinitely left and right every 'seconds'
  def bounce(self, seconds):

    def _bounce():
      if not self._stopped:
        self.every(seconds, _reverse)

    def _reverse():
      if not self._stopped:
        self._scroll_offset = -self._scroll_offset
        _bounce()

    _bounce()
    return self


  # Stops the scrolling of the contents
  def stop(self):
    self._stopped = True


  # Registers a scroll of the contents of a container by offset many places
  # A positive value scrolls the contents to a positive direction while a
  # negative one scrolls the contents to a negative direction, however these
  # directions are defined by the concrete child classes
  def _scroll(self, offset):
    self._scroll_offset = offset


  # Performs the actual scroll by the value previously specified with _scroll()
  # Child classes implement this method to provide custom scroll functionality
  # for all the different types of containers (_Buffer, _Line and _Cell)
  @abstractmethod
  def _perform_scroll(self):
    pass


# Class _Scroller END
################################################################################


# _ScreenScroller is a _Scroller helper class that provides an easy way to
# scroll the contents on the LCD screen line by line, if the _Buffer contains
# more lines than the screen itself
class _ScreenScroller(_Scroller):

  # Scrolls the contents in the buffer upwards
  # If 'by' value scrolls to an offset past the buffer contents, it simply
  # reaches the top of the scrolling (scrolls to the first line)
  # Returns self for call chaining
  def up(self, by=1):
    self._scroll( -by )
    return self


  # Scrolls the contents in the buffer downwards
  # If 'by' value scrolls to an offset past the buffer contents, it simply
  # reaches the bottom of the scrolling (scrolls to the last line)
  # Returns self for call chaining
  def down(self, by=1):
    self._scroll( +by )
    return self


  # Interfaces with the buffer to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_up(self, offset):
    return self.__buffer.scroll_up(offset)


  # Interfaces with the buffer to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_down(self, offset):
    return self.__buffer.scroll_down(offset)
  
  
  # Performs a scroll up of the contents on the screen by abs(_scroll_value)
  # lines if _scroll_value is negative, or a scroll down by the same amount
  # if positive
  def _perform_scroll(self):
    will_scroll_further = False

    if self._scroll_offset < 0:
      will_scroll_further =  self._scroll_up(abs(self._scroll_offset))
    else:
      will_scroll_further = self._scroll_down(self._scroll_offset)

    self._rewrite()
    return will_scroll_further


  # Initializes the _Scroller
  def __init__(self, buffer, rewrite):
    self.__buffer = buffer
    self._rewrite = rewrite
    self._scroll_offset = buffer._current_line_index
    self._stopped = False


# Class _ScreenScroller END
################################################################################


# _LineScroller is a _Scroller helper class that provides an easy way to
# scroll the contents of a _Line on the LCD screen cell by cell, if the _Line
# is longer than the screen's width
class _LineScroller(_Scroller):

  # Scrolls the contents in the _Line to the right
  # If 'by' value defines  an offset past the start of the _Line, it simply
  # scrolls to the first cell, ignoring the extra offset
  # Returns self for call chaining
  def right(self, by=1):
    self._scroll( -by )
    return self


  # Scrolls the contents in the _Line to the left
  # If 'by' value defines an offset past the end of the _Line, it simply
  # scrolls to the last cell, ignoring the extra offset
  # Returns self for call chaining
  def left(self, by=1):
    self._scroll( +by )
    return self


  # Interfaces with the _Line to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_right(self, offset):
    return self._line.scroll_right(offset)


  # Interfaces with the _Line to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_left(self, offset):
    return self._line.scroll_left(offset)
  
  
  # Performs a scroll of the contents to the right by abs(_scroll_value) cells
  # _scroll_offset is negative, or a scroll to the left by the same amount if
  # _scroll_offset is positive
  def _perform_scroll(self):
    will_scroll_further = False

    if self._scroll_offset < 0:
      will_scroll_further =  self._scroll_right(abs(self._scroll_offset))
    else:
      will_scroll_further = self._scroll_left(self._scroll_offset)

    if self._line.is_displayed(): self._rewrite()

    return will_scroll_further


  # Initializes the _Scroller
  def __init__(self, line, rewrite):
    self._line = line
    self._rewrite = rewrite
    self._scroll_offset = line._current_cell_index
    self._stopped = False


# Class _LineScroller END
################################################################################


# _CellScroller is a _Scroller helper class that provides an easy way to
# scroll the contents of a _Cwithin a _Line independently, character by
# character, if the text displayed in it is longer than its width
class _CellScroller(_Scroller):

  # Scrolls the contents in the _Cell to the right
  # If 'by' value defines an offset past the first character in the text,
  # it simply scrolls to the start, ignoring the extra offset
  # Returns self for call chaining
  def right(self, by=1):
    self._scroll( -by )
    return self


  # Scrolls the contents in the _Cell to the left
  # If 'by' value defines an offset past the cell contents, it simply
  # scrolls to the end, ignoring the extra offset
  # Returns self for call chaining
  def left(self, by=1):
    self._scroll( +by )
    return self


  # Interfaces with the _Cell to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_right(self, offset):
    return self._cell.scroll_right(offset)


  # Interfaces with the _Cellr to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_left(self, offset):
    return self._cell.scroll_left(offset)
  
  
  # Performs a scroll of the contents to the right by abs(_scroll_value)
  # characters _scroll_offset is negative, or a scroll to the left by the same
  # amount if _scroll_offset is positive
  def _perform_scroll(self):
    will_scroll_further = False

    if self._scroll_offset < 0:
      will_scroll_further =  self._scroll_right(abs(self._scroll_offset))
    else:
      will_scroll_further = self._scroll_left(self._scroll_offset)

    if self._cell.is_displayed(): self._rewrite()

    return will_scroll_further


  # Initializes the _Scroller
  def __init__(self, cell, rewrite):
    self._cell = cell
    self._rewrite = rewrite
    self._scroll_offset = cell._scroll_offset
    self._stopped = False


# Class _CellScroller END
################################################################################


from sys import modules

# Get a pointer to this module
this = modules[__name__]


# The buffer is used to internally hold and manipulate
# the string that is currently shown on the LCD
this.__buffer = _Buffer()


from ifc import HD44780


# Initializes controller and turns the display on
def init(pins=None, lines=None):
  global LCD_LINES
  LCD_LINES = lines or LCD_LINES

  HD44780.init(pins)
  HD44780.set_function( bit_mode = len(HD44780.__pins['db']),
                        num_lines = LCD_LINES   )
  HD44780.display_on()


# Clears the display of all text
def clear():
  HD44780.clear()


# Displays the given text on the LCD screen
# The text is saved and parsed in ythe _Buffer. You can provide an unlimited
# number of characters and lines. You can use the '\t' character to split a
# line into different cells and then set_tab_stops() to format their width.
def display(text):
  this.__buffer.parse(text)
  __rewrite()


# Formats the string currently displayed on the screen
# tab_stops are used to divide the screen into multiple parts (columns). These
# columns will display text in their own width and scroll independently. You
# can change between columns using the \t character when you provide the string
# to be written on the LCD
# Future development will include text alignment options
def format(tab_stops):

  if tab_stops:
    if all(isinstance(x, (int, long)) for x in tab_stops):
      for line in this.__buffer:
        line.set_tab_stops(tab_stops)
    elif len(tab_stops) == len(this.__buffer):
      for line in this.__buffer:
        line.set_tab_stops(tab_stops[ this.__buffer._current_line_index ])
    else:
      for n_tab_stops in tab_stops:
        n = len(n_tab_stops)
        for line in this.__buffer:
          if line.cell_count() - 1 == n:
            line.set_tab_stops(n_tab_stops)
  
  __rewrite()


# Shows or hides the cursor on the screen
def cursor(flags):
  HD44780.display_on( flags & CURSOR_VISIBLE,
                      flags & CURSOR_BLINK )


# Returns a specific _Scroller instance acording to the type of the parameter
# given
def scroll(what=None):

  if isinstance(what, _Line):
    return _LineScroller(what, __rewrite)
  elif isinstance(what, _Cell):
    return _CellScroller(what, __rewrite)
  else:
    return _ScreenScroller(this.__buffer, __rewrite)



# Returns the inner _Buffer object
def buffer():
  return this.__buffer


# Gets a _Line form the inner _Buffer
# If provided with an 'index' it returns the _Line at that index in the buffer
# If provided with an 'offset' it returns the _Line that is 'offset' many
# places away from the current _Line on the LCD screen
# If nothing is provided it returns the current _Line
def line(index=None, offset=None):

  if index:
    return this.__buffer[index]
  elif offset:
    return this.__buffer[ this.__buffer._current_line_index + offset ]
  else:
    return this.__buffer.line()


from threading import Lock


# Create a lock to secure the process of writing text on the screen because
# different threads created by multiple scrollers can mess it up
lock = Lock()


# Writes LCD_LINES many formatted _Lines from the _Buffer (if there are enough)
# to the LCD screen. This function blocks other threads from accessing it while
# it executes on some other thread
def __rewrite():
  lock.acquire()
  
  HD44780.clear()

  for offset in range(LCD_LINES):
    if line(offset=offset) == None: break
    HD44780.write(str(line(offset=offset)))
    HD44780.set_ddram_address(0x40)
  
  lock.release()


# Copyright (c) 2017 <NAME>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


'''
  This python module greatly extends the functionality of
  16x1 or 16x2 dot character LCD displays, using the HD44780
  controller or one alike, as well as it provides an easy way
  to interact with it. Briefly you can:
    * Display text on the LCD screen (duh)
    * Split the text into unlimited lines and lines into unlimited cells
    * Format cell widths using tab stops
    * Scroll through all of the contents with ease and in many different ways
    * Load extended dot patterns and draw them on the screen (coming soon)


  Author and Maintainer
  <NAME> <<EMAIL>>
'''


# Constants and Flags
# -------------------

# Current module version
VERSION = '0.7.1'

# Defines the number of lines on the LCD screen
LCD_LINES = 2

# Defines the number of characters per line
CHARS_PER_LINE = 16

# Flag to make cursor visible via the `cursor` function
CURSOR_VISIBLE = 0x01

# Flag to make cursor blink via the `cursor` function
CURSOR_BLINK = 0x02


# _Buffer is a helper class to control the module's
# internal text buffer. It parses the given text to lines
# ad holds an internal pointer to the current line
class _Buffer:

  # Parses the raw string internally and constructs the lines array
  # If 'string' is None, then it clears the buffer's contents
  def parse(self, string):
    self._lines = []
    self._raw = string

    if self._raw:
      for line in self._raw.split('\n'):
        self._lines += [ _Line(self).parse(line) ]
      
    self._reset()
    return self


  # Appends more lines at the end of the buffer
  def append(self, string):
    return self.parse(self._raw + '\n' + string)

  
  # Clears the buffer contents
  def clear(self):
    return self.parse(None)


  # Returns the _Line of text at index
  # If index is left blank, _current_line_index is used
  # If index exceeds line_count(), it returns None
  def line(self, index=None):
    if self._lines:
      index = self._current_line_index if index == None else index
      #return self._lines[ index % self.line_count() ]
      if index < self.line_count():
        return self._lines[ index ]
    
    return None


  # Returns the number of lines in the buffer
  def line_count(self):
    return len(self._lines)


  # Returns True if the given _Line is currently displayed on the LCD
  # Returns False otherwise
  def is_displayed(self, line):
    index = self._lines.index(line)
    if index is not None and index in range(self._current_line_index,\
                                self._current_line_index + LCD_LINES):
      return True
    else: return False


  # Returns True if contents can scroll to 'position', False otherwise
  # In other words True is returned if 'position' is within [0, line_count())
  def scrolls_to(self, position):
    return position in range(self.line_count())


  # Scrolls to the line at 'position'
  # If 'position' is negative it scrolls to the first line, whereas
  # if 'position' is equal or greater line_count(), it scrolls to the last
  # _Line
  def scroll_to(self, position):
    if self.scrolls_to(position):
      self._current_line_index = position
    else:
      self.scroll_top() if position < 0 else self.scroll_bottom()


  # Scrolls the contents down by 'offset' lines
  # Returns True if contents will further scroll to that direction by the same
  # amount, False otherwise
  def scroll_down(self, offset=1):
    self.scroll_to( self._current_line_index + offset )
    return self.scrolls_to( self._current_line_index + offset )


  # Scrolls the contents up by 'offset' lines
  # Returns True if contents will further scroll to that direction by the same
  # amount, False otherwise
  def scroll_up(self, offset=1):
    self.scroll_to( self._current_line_index - offset )
    return self.scrolls_to( self._current_line_index - offset )


  # Scrolls the contents to the first line in the buffer
  def scroll_top(self):
    self.scroll_to(0)


  # Scrolls the contents to the last line in the buffer
  def scroll_bottom(self):
    self.scroll_to(self.line_count() - 1)


  # Concatenates all the lines in the buffer into one formatted string
  # Mainly to be called from __str__() so users are able to print the
  # buffer on the computer without any effort
  def _format_contents(self):
    if self._lines:
      formatted = ""

      for i in range(LCD_LINES):
        formatted += str(self[ self._current_line_index + i ]) + '\n'

      return formatted


  # Resets the internal line index
  def _reset(self):
    self._current_line_index = 0


  # Initializes the _Buffer
  def __init__(self):
    self._lines = []
    self._current_line_index = 0

  
  # Uses _format_contents to return a formatted string of its contents
  # Falls back to its name
  def __str__(self):
    return self._format_contents() or _Buffer.__name__


  # Overloads operator.len() to return the _Buffer's line_count()
  def __len__(self):
    return self.line_count()


  # Overloads operator [] to access a line in the buffer
  def __getitem__(self, index):
    return self.line(index)


  # Returns the internal _lines array when involved in for...in loops
  def __iter__(self):
    return iter(self._lines)


# Class _Buffer END
################################################################################


# Helper class that formats a given line of string for display
# on the lcd screen
# The text in the line can be split into multiple cells using tab stops
# Each cell is displayed and scrolled seperately on the screen
class _Line:

  # Parses the given string
  def parse(self, string):
    self._cells = []
    self._raw = string

    if self._raw:
      cells = self._raw.split('\t')

      for cell in cells:
        self._cells += [ _Cell(self, text=cell) ]
      
      self._auto_tab_stops()
    
    self._reset()
    return self


  # Returns the text contained in cell at index
  # If index is left blank, the internal cell index is used
  # If index exceeds cell_count(), it circles back to the beginning
  def cell(self, index=None):
    if self._cells:
      index = self._current_cell_index if index == None else index
      # return self._cells[ index % self.cell_count() ]
      if index < self.cell_count():
        return self._cells[ index ]
    
    return None


  # Returns the number of cells in the last parsed line of text
  # That typically corresponds to the number of '\t' characters
  # in the line plus 1
  def cell_count(self):
    return len(self._cells)


  # Lets the user set custom tab stops
  # Tab stops are used to control the number and width of cells
  # in a line of text. These cells will be displayed and scrolled
  # independently. You can change between cells using the '\t'
  # character when you provide the string to be written on the LCD screen
  def set_tab_stops(self, tab_stops):
    if len(tab_stops) < (len(self._cells) - 1):
      raise ValueError("Fewer tab stops given than tab characters in line")
    if not all(x < y for x, y in zip(tab_stops, tab_stops[1:])):
      raise ValueError("Tab stops must be in ascending order")
 
    self._tab_stops = tab_stops
    self._distribute_cell_widths()


  # Returns True if this _Line is currently displayed on the LCD screen
  # Returns False otherwise
  def is_displayed(self, cell=None):
    if cell:
      index = self._cells.index(cell)
    
      if self.is_displayed() and index in range(self._current_cell_index,\
                   self._current_cell_index + self._last_visible_cell_index):
        return True
      else: return False

    else: return self._parent.is_displayed(self)


  # Returns True if contents can scroll to 'position', False otherwise
  # In other words True is returned if 'position' is within [0, cell_count())
  def scrolls_to(self, position):
    return position in range(self.cell_count())


  # Scrolls to the cell at 'position'
  # If 'position' is negative it scrolls to the first cell, whereas
  # if 'position' is equal or grater than cell_count(), it scrolls to the last
  # cell
  def scroll_to(self, position):
    if self.scrolls_to(position):
      self._current_cell_index = position
    else:
      self.scroll_start() if position < 0 else self.scroll_end()


  # Scrolls the contents to the left by 'offset' cells
  def scroll_left(self, offset=1):
    self.scroll_to( self._current_cell_index + offset )
    return self.scrolls_to( self._current_cell_index + offset )


  # Scrolls the contents to the right by 'offset' cells
  def scroll_right(self, offset=1):
    self.scroll_to( self._current_cell_index - offset )
    return self.scrolls_to( self._current_cell_index - offset )


  # Scrolls to the start of the line
  def scroll_start(self):
    self.scroll_to(0)


  # Scrolls to the end of the line
  def scroll_end(self):
    self.scroll_to(self.cell_count() - 1)


  # Turns its contents into a formatted string to be written on the LCD screen
  def _format_contents(self, tab_stops=None):
    formatted = ""

    if self._cells:
      for cell in self._cells[self._current_cell_index:]:
        if len(formatted) >= CHARS_PER_LINE: break
        formatted += str(cell)
        self._last_visible_cell_index = self._cells.index(cell)

      return formatted[:CHARS_PER_LINE]


  # Resets the internal cell index
  def _reset(self):
    self._current_cell_index = 0
    self._last_visible_cell_index = 0


  # Generates evenly spaced tab stops based on their count
  # Up to 16 tab stops can be generated automatically and fitted
  # within the 16-character line of the lcd screen. If their width
  # cannot be divided exactly to an integer amount, it will be floored
  # to the nearest integer, so contents of the last cell might be
  # partly or totally hidden, and you should call scroll_left() on the
  # line to make them visible
  def _auto_tab_stops(self):
    count = min(self.cell_count(), 16)
    tab_stops = [] 
    for i in range(count - 1):
      tab_stops += [(16 / count) * (i + 1)]

    self.set_tab_stops(tab_stops)
  
  
  # Distributes the width of each cell in the line
  def _distribute_cell_widths(self):
    for cell in self._cells:
      index = self._cells.index(cell)
      cell.set_width( self._calculate_cell_width(index) )


  # Calculates the width of a cell based on its bounding tab stops
  # If index is left blank, _current_cell_index will be used.
  # Notice that this function does not check if index is out of bounds
  def _calculate_cell_width(self, index):
    last = ([0] + self._tab_stops)[-1]
    padding_end = 16 - (last % 16)
    cell_start = ([0] + self._tab_stops)[index]
    cell_end   = (self._tab_stops + [last + padding_end])[index]
    return cell_end - cell_start
     

  # Initializes the _Line
  def __init__(self, parent):
    if not isinstance(parent, _Buffer):
      raise TypeError("Parent should be an instance of _Buffer")
    self._parent = parent
    self.parse(None)


  # Returns the formatted contents of the line that are to be written
  # on the lcd screen, or __name__ if no line is yet parsed
  def __str__(self):
    return self._format_contents() or _Line.__name__


  # Returns the _Line's cumulative sum of its cell widths
  def __len__(self):
    length = 0
    for cell in self._cells:
      length += cell.get_width()
    return length


  # Makes it possible to access a _Cell by applying indexing on the _Line
  def __getitem__(self, index):
    return self.cell(index)


  # The next two methods implement the iterator protocol in
  # order to make instaces of this class iterable. Users can
  # therefore iterate between cells in a line easily in a
  # for...in loop
  def __iter__(self):
    return iter(self._cells)


# Class _Line END
################################################################################


class _Cell:

  # Sets the width of the cell
  def set_width(self, width):
    if width < 1 or width > CHARS_PER_LINE:
      raise ValueError("Width of cell should be between 1 and {} characters"\
                                                      .format(CHARS_PER_LINE))

    self._width = width
    self.scroll_to(0)


  # Returns the width of the cell
  def get_width(self):
    return self._width


  # Returns true if the _Cell is currently displayed on the LCD screen
  # Returns False otherwise
  def is_displayed(self):
    return self._parent.is_displayed(self)


  # Returns True if contents can scroll to 'position', False otherwise
  # Contents can scroll until there are no more hidden characters past the
  # right boundary of the cell
  def scrolls_to(self, position):
    return position in range(len(self.text) - self.get_width() + 1)


  # Scrolls the text to the character at 'position' within the displayed string
  # If 'position' is zero or negative, it scrolls to the top, whereas if
  # 'position' causes the last character in the text to be displayed before the
  # right boundary of the cell (displaces the text too much), it scrolls to the
  # end
  def scroll_to(self, position):
    if self.scrolls_to(position):
      self._scroll_offset = position
    elif len(self.text) > self.get_width():
      self.scroll_start() if position < 0 else self.scroll_end()


  # Scrolls the contents to the left by 'offset' characters
  def scroll_left(self, offset=1):
    self.scroll_to( self._scroll_offset + offset )
    return self.scrolls_to( self._scroll_offset + offset )


  # Scrolls the contents to the right by 'offset' characters
  def scroll_right(self, offset=1):
    self.scroll_to( self._scroll_offset - offset )
    return self.scrolls_to( self._scroll_offset - offset )


  # Scrolls to the start of the text
  def scroll_start(self):
    self.scroll_to(0)


  # Scrolls to the end of the text
  def scroll_end(self):
    self.scroll_to(len(self.text) - self.get_width())


  # Returns the portion of text that fits inside the cell. If the
  # text length is larger than the cell's width, it can be scrolled
  # using scroll_right(), scroll_left() or scroll_to() methods
  def _format_contents(self):
    start = self._scroll_offset
    end = start + self.get_width()
    return self.text[ start : end ].ljust(self.get_width())


  # Initializes the cell with some text and specifies its width
  # (16 characters by default)
  def __init__(self, parent, text, width=16):
    if not isinstance(parent, _Line):
      raise TypeError("Parent must be an instance of _Line")
    self._parent = parent
    self.text = text
    self.set_width(width)
    self._scroll_offset = 0


  def __str__(self):
    return self._format_contents() or _Cell.__name__


# Class _Cell END
################################################################################


from abc import ABCMeta, abstractmethod
from threading import Timer


# _Scroller is a base class that serves as a common ancestor of the concrete
# implementation of all the _Scroller classes. It provides the common interface
# used by _ScrollerBuilder
class _Scroller:

  # Make this an abstract class 
  __metaclass__ = ABCMeta


  # Performs the scrolling once
  def once(self):
    self._perform_scroll()


  # Performs the scrolling repeatedly every 'seconds'
  # This method starts a new Timer that calls the same function after the given
  # time delay in seconds, until the contents of the container can no longer
  # scroll. If circular is set to True, the scrolling is going to repeat back
  # and forth forever until stop() is called
  def every(self, seconds, callback=None):
    if not self._stopped:
      if self._perform_scroll():
        Timer(seconds, self.every, [seconds, callback]).start()
      elif callback: Timer(seconds, callback).start()
    else: self._stopped = False

    return self

  # Scrolls indefinitely left and right every 'seconds'
  def bounce(self, seconds):

    def _bounce():
      if not self._stopped:
        self.every(seconds, _reverse)

    def _reverse():
      if not self._stopped:
        self._scroll_offset = -self._scroll_offset
        _bounce()

    _bounce()
    return self


  # Stops the scrolling of the contents
  def stop(self):
    self._stopped = True


  # Registers a scroll of the contents of a container by offset many places
  # A positive value scrolls the contents to a positive direction while a
  # negative one scrolls the contents to a negative direction, however these
  # directions are defined by the concrete child classes
  def _scroll(self, offset):
    self._scroll_offset = offset


  # Performs the actual scroll by the value previously specified with _scroll()
  # Child classes implement this method to provide custom scroll functionality
  # for all the different types of containers (_Buffer, _Line and _Cell)
  @abstractmethod
  def _perform_scroll(self):
    pass


# Class _Scroller END
################################################################################


# _ScreenScroller is a _Scroller helper class that provides an easy way to
# scroll the contents on the LCD screen line by line, if the _Buffer contains
# more lines than the screen itself
class _ScreenScroller(_Scroller):

  # Scrolls the contents in the buffer upwards
  # If 'by' value scrolls to an offset past the buffer contents, it simply
  # reaches the top of the scrolling (scrolls to the first line)
  # Returns self for call chaining
  def up(self, by=1):
    self._scroll( -by )
    return self


  # Scrolls the contents in the buffer downwards
  # If 'by' value scrolls to an offset past the buffer contents, it simply
  # reaches the bottom of the scrolling (scrolls to the last line)
  # Returns self for call chaining
  def down(self, by=1):
    self._scroll( +by )
    return self


  # Interfaces with the buffer to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_up(self, offset):
    return self.__buffer.scroll_up(offset)


  # Interfaces with the buffer to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_down(self, offset):
    return self.__buffer.scroll_down(offset)
  
  
  # Performs a scroll up of the contents on the screen by abs(_scroll_value)
  # lines if _scroll_value is negative, or a scroll down by the same amount
  # if positive
  def _perform_scroll(self):
    will_scroll_further = False

    if self._scroll_offset < 0:
      will_scroll_further =  self._scroll_up(abs(self._scroll_offset))
    else:
      will_scroll_further = self._scroll_down(self._scroll_offset)

    self._rewrite()
    return will_scroll_further


  # Initializes the _Scroller
  def __init__(self, buffer, rewrite):
    self.__buffer = buffer
    self._rewrite = rewrite
    self._scroll_offset = buffer._current_line_index
    self._stopped = False


# Class _ScreenScroller END
################################################################################


# _LineScroller is a _Scroller helper class that provides an easy way to
# scroll the contents of a _Line on the LCD screen cell by cell, if the _Line
# is longer than the screen's width
class _LineScroller(_Scroller):

  # Scrolls the contents in the _Line to the right
  # If 'by' value defines  an offset past the start of the _Line, it simply
  # scrolls to the first cell, ignoring the extra offset
  # Returns self for call chaining
  def right(self, by=1):
    self._scroll( -by )
    return self


  # Scrolls the contents in the _Line to the left
  # If 'by' value defines an offset past the end of the _Line, it simply
  # scrolls to the last cell, ignoring the extra offset
  # Returns self for call chaining
  def left(self, by=1):
    self._scroll( +by )
    return self


  # Interfaces with the _Line to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_right(self, offset):
    return self._line.scroll_right(offset)


  # Interfaces with the _Line to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_left(self, offset):
    return self._line.scroll_left(offset)
  
  
  # Performs a scroll of the contents to the right by abs(_scroll_value) cells
  # _scroll_offset is negative, or a scroll to the left by the same amount if
  # _scroll_offset is positive
  def _perform_scroll(self):
    will_scroll_further = False

    if self._scroll_offset < 0:
      will_scroll_further =  self._scroll_right(abs(self._scroll_offset))
    else:
      will_scroll_further = self._scroll_left(self._scroll_offset)

    if self._line.is_displayed(): self._rewrite()

    return will_scroll_further


  # Initializes the _Scroller
  def __init__(self, line, rewrite):
    self._line = line
    self._rewrite = rewrite
    self._scroll_offset = line._current_cell_index
    self._stopped = False


# Class _LineScroller END
################################################################################


# _CellScroller is a _Scroller helper class that provides an easy way to
# scroll the contents of a _Cwithin a _Line independently, character by
# character, if the text displayed in it is longer than its width
class _CellScroller(_Scroller):

  # Scrolls the contents in the _Cell to the right
  # If 'by' value defines an offset past the first character in the text,
  # it simply scrolls to the start, ignoring the extra offset
  # Returns self for call chaining
  def right(self, by=1):
    self._scroll( -by )
    return self


  # Scrolls the contents in the _Cell to the left
  # If 'by' value defines an offset past the cell contents, it simply
  # scrolls to the end, ignoring the extra offset
  # Returns self for call chaining
  def left(self, by=1):
    self._scroll( +by )
    return self


  # Interfaces with the _Cell to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_right(self, offset):
    return self._cell.scroll_right(offset)


  # Interfaces with the _Cellr to perform the actual scrolling
  # Returns True if contents can further scroll to that direction,
  # False otherwise
  def _scroll_left(self, offset):
    return self._cell.scroll_left(offset)
  
  
  # Performs a scroll of the contents to the right by abs(_scroll_value)
  # characters _scroll_offset is negative, or a scroll to the left by the same
  # amount if _scroll_offset is positive
  def _perform_scroll(self):
    will_scroll_further = False

    if self._scroll_offset < 0:
      will_scroll_further =  self._scroll_right(abs(self._scroll_offset))
    else:
      will_scroll_further = self._scroll_left(self._scroll_offset)

    if self._cell.is_displayed(): self._rewrite()

    return will_scroll_further


  # Initializes the _Scroller
  def __init__(self, cell, rewrite):
    self._cell = cell
    self._rewrite = rewrite
    self._scroll_offset = cell._scroll_offset
    self._stopped = False


# Class _CellScroller END
################################################################################


from sys import modules

# Get a pointer to this module
this = modules[__name__]


# The buffer is used to internally hold and manipulate
# the string that is currently shown on the LCD
this.__buffer = _Buffer()


from ifc import HD44780


# Initializes controller and turns the display on
def init(pins=None, lines=None):
  global LCD_LINES
  LCD_LINES = lines or LCD_LINES

  HD44780.init(pins)
  HD44780.set_function( bit_mode = len(HD44780.__pins['db']),
                        num_lines = LCD_LINES   )
  HD44780.display_on()


# Clears the display of all text
def clear():
  HD44780.clear()


# Displays the given text on the LCD screen
# The text is saved and parsed in ythe _Buffer. You can provide an unlimited
# number of characters and lines. You can use the '\t' character to split a
# line into different cells and then set_tab_stops() to format their width.
def display(text):
  this.__buffer.parse(text)
  __rewrite()


# Formats the string currently displayed on the screen
# tab_stops are used to divide the screen into multiple parts (columns). These
# columns will display text in their own width and scroll independently. You
# can change between columns using the \t character when you provide the string
# to be written on the LCD
# Future development will include text alignment options
def format(tab_stops):

  if tab_stops:
    if all(isinstance(x, (int, long)) for x in tab_stops):
      for line in this.__buffer:
        line.set_tab_stops(tab_stops)
    elif len(tab_stops) == len(this.__buffer):
      for line in this.__buffer:
        line.set_tab_stops(tab_stops[ this.__buffer._current_line_index ])
    else:
      for n_tab_stops in tab_stops:
        n = len(n_tab_stops)
        for line in this.__buffer:
          if line.cell_count() - 1 == n:
            line.set_tab_stops(n_tab_stops)
  
  __rewrite()


# Shows or hides the cursor on the screen
def cursor(flags):
  HD44780.display_on( flags & CURSOR_VISIBLE,
                      flags & CURSOR_BLINK )


# Returns a specific _Scroller instance acording to the type of the parameter
# given
def scroll(what=None):

  if isinstance(what, _Line):
    return _LineScroller(what, __rewrite)
  elif isinstance(what, _Cell):
    return _CellScroller(what, __rewrite)
  else:
    return _ScreenScroller(this.__buffer, __rewrite)



# Returns the inner _Buffer object
def buffer():
  return this.__buffer


# Gets a _Line form the inner _Buffer
# If provided with an 'index' it returns the _Line at that index in the buffer
# If provided with an 'offset' it returns the _Line that is 'offset' many
# places away from the current _Line on the LCD screen
# If nothing is provided it returns the current _Line
def line(index=None, offset=None):

  if index:
    return this.__buffer[index]
  elif offset:
    return this.__buffer[ this.__buffer._current_line_index + offset ]
  else:
    return this.__buffer.line()


from threading import Lock


# Create a lock to secure the process of writing text on the screen because
# different threads created by multiple scrollers can mess it up
lock = Lock()


# Writes LCD_LINES many formatted _Lines from the _Buffer (if there are enough)
# to the LCD screen. This function blocks other threads from accessing it while
# it executes on some other thread
def __rewrite():
  lock.acquire()
  
  HD44780.clear()

  for offset in range(LCD_LINES):
    if line(offset=offset) == None: break
    HD44780.write(str(line(offset=offset)))
    HD44780.set_ddram_address(0x40)
  
  lock.release()
# Copyright (c) 2017 <NAME> # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. This python module greatly extends the functionality of   16x1 or 16x2 dot character LCD displays, using the HD44780   controller or one alike, as well as it provides an easy way   to interact with it. Briefly you can:     * Display text on the LCD screen (duh)     * Split the text into unlimited lines and lines into unlimited cells     * Format cell widths using tab stops     * Scroll through all of the contents with ease and in many different ways     * Load extended dot patterns and draw them on the screen (coming soon)     Author and Maintainer   <NAME> <<EMAIL>> # Constants and Flags # ------------------- # Current module version # Defines the number of lines on the LCD screen # Defines the number of characters per line # Flag to make cursor visible via the `cursor` function # Flag to make cursor blink via the `cursor` function # _Buffer is a helper class to control the module's # internal text buffer. It parses the given text to lines # ad holds an internal pointer to the current line # Parses the raw string internally and constructs the lines array # If 'string' is None, then it clears the buffer's contents # Appends more lines at the end of the buffer # Clears the buffer contents # Returns the _Line of text at index # If index is left blank, _current_line_index is used # If index exceeds line_count(), it returns None #return self._lines[ index % self.line_count() ] # Returns the number of lines in the buffer # Returns True if the given _Line is currently displayed on the LCD # Returns False otherwise # Returns True if contents can scroll to 'position', False otherwise # In other words True is returned if 'position' is within [0, line_count()) # Scrolls to the line at 'position' # If 'position' is negative it scrolls to the first line, whereas # if 'position' is equal or greater line_count(), it scrolls to the last # _Line # Scrolls the contents down by 'offset' lines # Returns True if contents will further scroll to that direction by the same # amount, False otherwise # Scrolls the contents up by 'offset' lines # Returns True if contents will further scroll to that direction by the same # amount, False otherwise # Scrolls the contents to the first line in the buffer # Scrolls the contents to the last line in the buffer # Concatenates all the lines in the buffer into one formatted string # Mainly to be called from __str__() so users are able to print the # buffer on the computer without any effort # Resets the internal line index # Initializes the _Buffer # Uses _format_contents to return a formatted string of its contents # Falls back to its name # Overloads operator.len() to return the _Buffer's line_count() # Overloads operator [] to access a line in the buffer # Returns the internal _lines array when involved in for...in loops # Class _Buffer END ################################################################################ # Helper class that formats a given line of string for display # on the lcd screen # The text in the line can be split into multiple cells using tab stops # Each cell is displayed and scrolled seperately on the screen # Parses the given string # Returns the text contained in cell at index # If index is left blank, the internal cell index is used # If index exceeds cell_count(), it circles back to the beginning # return self._cells[ index % self.cell_count() ] # Returns the number of cells in the last parsed line of text # That typically corresponds to the number of '\t' characters # in the line plus 1 # Lets the user set custom tab stops # Tab stops are used to control the number and width of cells # in a line of text. These cells will be displayed and scrolled # independently. You can change between cells using the '\t' # character when you provide the string to be written on the LCD screen # Returns True if this _Line is currently displayed on the LCD screen # Returns False otherwise # Returns True if contents can scroll to 'position', False otherwise # In other words True is returned if 'position' is within [0, cell_count()) # Scrolls to the cell at 'position' # If 'position' is negative it scrolls to the first cell, whereas # if 'position' is equal or grater than cell_count(), it scrolls to the last # cell # Scrolls the contents to the left by 'offset' cells # Scrolls the contents to the right by 'offset' cells # Scrolls to the start of the line # Scrolls to the end of the line # Turns its contents into a formatted string to be written on the LCD screen # Resets the internal cell index # Generates evenly spaced tab stops based on their count # Up to 16 tab stops can be generated automatically and fitted # within the 16-character line of the lcd screen. If their width # cannot be divided exactly to an integer amount, it will be floored # to the nearest integer, so contents of the last cell might be # partly or totally hidden, and you should call scroll_left() on the # line to make them visible # Distributes the width of each cell in the line # Calculates the width of a cell based on its bounding tab stops # If index is left blank, _current_cell_index will be used. # Notice that this function does not check if index is out of bounds # Initializes the _Line # Returns the formatted contents of the line that are to be written # on the lcd screen, or __name__ if no line is yet parsed # Returns the _Line's cumulative sum of its cell widths # Makes it possible to access a _Cell by applying indexing on the _Line # The next two methods implement the iterator protocol in # order to make instaces of this class iterable. Users can # therefore iterate between cells in a line easily in a # for...in loop # Class _Line END ################################################################################ # Sets the width of the cell # Returns the width of the cell # Returns true if the _Cell is currently displayed on the LCD screen # Returns False otherwise # Returns True if contents can scroll to 'position', False otherwise # Contents can scroll until there are no more hidden characters past the # right boundary of the cell # Scrolls the text to the character at 'position' within the displayed string # If 'position' is zero or negative, it scrolls to the top, whereas if # 'position' causes the last character in the text to be displayed before the # right boundary of the cell (displaces the text too much), it scrolls to the # end # Scrolls the contents to the left by 'offset' characters # Scrolls the contents to the right by 'offset' characters # Scrolls to the start of the text # Scrolls to the end of the text # Returns the portion of text that fits inside the cell. If the # text length is larger than the cell's width, it can be scrolled # using scroll_right(), scroll_left() or scroll_to() methods # Initializes the cell with some text and specifies its width # (16 characters by default) # Class _Cell END ################################################################################ # _Scroller is a base class that serves as a common ancestor of the concrete # implementation of all the _Scroller classes. It provides the common interface # used by _ScrollerBuilder # Make this an abstract class # Performs the scrolling once # Performs the scrolling repeatedly every 'seconds' # This method starts a new Timer that calls the same function after the given # time delay in seconds, until the contents of the container can no longer # scroll. If circular is set to True, the scrolling is going to repeat back # and forth forever until stop() is called # Scrolls indefinitely left and right every 'seconds' # Stops the scrolling of the contents # Registers a scroll of the contents of a container by offset many places # A positive value scrolls the contents to a positive direction while a # negative one scrolls the contents to a negative direction, however these # directions are defined by the concrete child classes # Performs the actual scroll by the value previously specified with _scroll() # Child classes implement this method to provide custom scroll functionality # for all the different types of containers (_Buffer, _Line and _Cell) # Class _Scroller END ################################################################################ # _ScreenScroller is a _Scroller helper class that provides an easy way to # scroll the contents on the LCD screen line by line, if the _Buffer contains # more lines than the screen itself # Scrolls the contents in the buffer upwards # If 'by' value scrolls to an offset past the buffer contents, it simply # reaches the top of the scrolling (scrolls to the first line) # Returns self for call chaining # Scrolls the contents in the buffer downwards # If 'by' value scrolls to an offset past the buffer contents, it simply # reaches the bottom of the scrolling (scrolls to the last line) # Returns self for call chaining # Interfaces with the buffer to perform the actual scrolling # Returns True if contents can further scroll to that direction, # False otherwise # Interfaces with the buffer to perform the actual scrolling # Returns True if contents can further scroll to that direction, # False otherwise # Performs a scroll up of the contents on the screen by abs(_scroll_value) # lines if _scroll_value is negative, or a scroll down by the same amount # if positive # Initializes the _Scroller # Class _ScreenScroller END ################################################################################ # _LineScroller is a _Scroller helper class that provides an easy way to # scroll the contents of a _Line on the LCD screen cell by cell, if the _Line # is longer than the screen's width # Scrolls the contents in the _Line to the right # If 'by' value defines  an offset past the start of the _Line, it simply # scrolls to the first cell, ignoring the extra offset # Returns self for call chaining # Scrolls the contents in the _Line to the left # If 'by' value defines an offset past the end of the _Line, it simply # scrolls to the last cell, ignoring the extra offset # Returns self for call chaining # Interfaces with the _Line to perform the actual scrolling # Returns True if contents can further scroll to that direction, # False otherwise # Interfaces with the _Line to perform the actual scrolling # Returns True if contents can further scroll to that direction, # False otherwise # Performs a scroll of the contents to the right by abs(_scroll_value) cells # _scroll_offset is negative, or a scroll to the left by the same amount if # _scroll_offset is positive # Initializes the _Scroller # Class _LineScroller END ################################################################################ # _CellScroller is a _Scroller helper class that provides an easy way to # scroll the contents of a _Cwithin a _Line independently, character by # character, if the text displayed in it is longer than its width # Scrolls the contents in the _Cell to the right # If 'by' value defines an offset past the first character in the text, # it simply scrolls to the start, ignoring the extra offset # Returns self for call chaining # Scrolls the contents in the _Cell to the left # If 'by' value defines an offset past the cell contents, it simply # scrolls to the end, ignoring the extra offset # Returns self for call chaining # Interfaces with the _Cell to perform the actual scrolling # Returns True if contents can further scroll to that direction, # False otherwise # Interfaces with the _Cellr to perform the actual scrolling # Returns True if contents can further scroll to that direction, # False otherwise # Performs a scroll of the contents to the right by abs(_scroll_value) # characters _scroll_offset is negative, or a scroll to the left by the same # amount if _scroll_offset is positive # Initializes the _Scroller # Class _CellScroller END ################################################################################ # Get a pointer to this module # The buffer is used to internally hold and manipulate # the string that is currently shown on the LCD # Initializes controller and turns the display on # Clears the display of all text # Displays the given text on the LCD screen # The text is saved and parsed in ythe _Buffer. You can provide an unlimited # number of characters and lines. You can use the '\t' character to split a # line into different cells and then set_tab_stops() to format their width. # Formats the string currently displayed on the screen # tab_stops are used to divide the screen into multiple parts (columns). These # columns will display text in their own width and scroll independently. You # can change between columns using the \t character when you provide the string # to be written on the LCD # Future development will include text alignment options # Shows or hides the cursor on the screen # Returns a specific _Scroller instance acording to the type of the parameter # given # Returns the inner _Buffer object # Gets a _Line form the inner _Buffer # If provided with an 'index' it returns the _Line at that index in the buffer # If provided with an 'offset' it returns the _Line that is 'offset' many # places away from the current _Line on the LCD screen # If nothing is provided it returns the current _Line # Create a lock to secure the process of writing text on the screen because # different threads created by multiple scrollers can mess it up # Writes LCD_LINES many formatted _Lines from the _Buffer (if there are enough) # to the LCD screen. This function blocks other threads from accessing it while # it executes on some other thread