├── requirements.txt ├── rpilcdmenu ├── selection_menu.py ├── items │ ├── select_item.py │ ├── range_selection_item.py │ ├── __init__.py │ ├── submenu_item.py │ ├── message_item.py │ ├── function_item.py │ └── menu_item.py ├── version.py ├── views │ ├── __init__.py │ └── message_view.py ├── helpers │ ├── __init__.py │ └── text_helper.py ├── __init__.py ├── rpi_lcd_submenu.py ├── rpi_lcd_menu.py ├── base_menu.py └── rpi_lcd_hwd.py ├── examples ├── rpilcdmenu ├── example.py ├── example2.py ├── example3.py ├── example5.py └── example4.py ├── doc ├── configuration.png └── rpi-example.jpg ├── requirements-test.txt ├── .coveragerc ├── README.txt ├── .gitignore ├── MANIFEST ├── .travis.yml ├── setup.py ├── tests └── unit │ └── rpilcdmenu │ ├── test_rpi_lcd_submenu.py │ ├── items │ ├── test_submenu_item.py │ ├── test_message_item.py │ ├── test_function_item.py │ └── test_menu_item.py │ ├── helpers │ └── test_text_helper.py │ ├── test_base_menu.py │ ├── views │ └── test_message_view.py │ ├── test_rpi_lcd_hwd.py │ └── test_rpi_lcd_menu.py ├── tox.ini └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rpilcdmenu/selection_menu.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/rpilcdmenu: -------------------------------------------------------------------------------- 1 | ../rpilcdmenu/ -------------------------------------------------------------------------------- /rpilcdmenu/items/select_item.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rpilcdmenu/items/range_selection_item.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rpilcdmenu/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1" 2 | -------------------------------------------------------------------------------- /doc/configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dublerq/rpi-lcd-menu/HEAD/doc/configuration.png -------------------------------------------------------------------------------- /doc/rpi-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dublerq/rpi-lcd-menu/HEAD/doc/rpi-example.jpg -------------------------------------------------------------------------------- /rpilcdmenu/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .message_view import MessageView 2 | 3 | __all__ = ['MessageView'] 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | codecov 2 | mock 3 | pytest 4 | pytest-cov 5 | pytest-timeout 6 | tox 7 | tox-travis 8 | -------------------------------------------------------------------------------- /rpilcdmenu/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from .text_helper import get_scrolled_text 2 | 3 | __all__ = ['get_scrolled_text'] 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */__init__* 3 | rpilcdmenu/version.py 4 | setup.py 5 | tests/* 6 | 7 | [html] 8 | directory = coverage 9 | 10 | [xml] 11 | output = coverage.xml 12 | -------------------------------------------------------------------------------- /rpilcdmenu/__init__.py: -------------------------------------------------------------------------------- 1 | from .rpi_lcd_menu import RpiLCDMenu 2 | from .rpi_lcd_submenu import RpiLCDSubMenu 3 | from .version import __version__ 4 | 5 | __all__ = ['RpiLCDMenu', 'RpiLCDSubMenu', 'items', 'views'] 6 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | RPI LCD Menu is a python library for creating multi level menus displayed on 16x2 LCD screens (i.e. hd44780). 2 | Navigation can be easily implemented for any user input (buttons, joysticks, switches, detectors etc.). 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dynamically generated files we don't want to keep 2 | *.pyc 3 | *.sw? 4 | .tox 5 | .env 6 | .eggs 7 | *.pyo 8 | *.bak 9 | *.xml 10 | .cache 11 | coverage 12 | .coverage 13 | docs/build 14 | .idea 15 | *.egg-info 16 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | README.txt 3 | setup.py 4 | rpilcdmenu/__init__.py 5 | rpilcdmenu/basic_menu.py 6 | rpilcdmenu/rpi_lcd_menu.py 7 | rpilcdmenu/rpi_lcd_submenu.py 8 | rpilcdmenu/selection_menu.py 9 | rpilcdmenu/version.py 10 | -------------------------------------------------------------------------------- /rpilcdmenu/items/__init__.py: -------------------------------------------------------------------------------- 1 | from .menu_item import MenuItem 2 | from .function_item import FunctionItem 3 | from .submenu_item import SubmenuItem 4 | from .message_item import MessageItem 5 | 6 | __all__ = ['FunctionItem', 'SubmenuItem', 'MenuItem', 'MessageItem'] 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | python: 6 | - 2.7 7 | - 3.4 8 | - 3.5 9 | - 3.6 10 | 11 | install: 12 | - pip install -Ur requirements-test.txt 13 | - pip install -Ue . 14 | 15 | script: tox -- --cov --no-cov-on-fail --cov-report= 16 | 17 | after_success: codecov -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='RpiLCDMenu', 5 | version='0.0.1dev', 6 | packages=find_packages(), 7 | license='Creative Commons Attribution-Noncommercial-Share Alike license', 8 | long_description=open('README.txt').read(), 9 | ) 10 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | test if lcd display is connected to raspberry on default pins 5 | """ 6 | 7 | from rpilcdmenu import * 8 | 9 | def main(): 10 | menu = RpiLCDMenu(26,19,[13, 6, 5, 21]) 11 | menu.displayTestScreen() 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /rpilcdmenu/rpi_lcd_submenu.py: -------------------------------------------------------------------------------- 1 | from rpilcdmenu import RpiLCDMenu 2 | 3 | 4 | class RpiLCDSubMenu(RpiLCDMenu): 5 | def __init__(self, base_menu): 6 | """ 7 | Initialize SubMenu 8 | """ 9 | self.lcd = base_menu.lcd 10 | 11 | super(RpiLCDMenu, self).__init__(base_menu) 12 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/test_rpi_lcd_submenu.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from rpilcdmenu.rpi_lcd_submenu import RpiLCDSubMenu 4 | 5 | 6 | def test_rpilcdmenu_can_be_initialized(): 7 | base_menu_mock = mock.Mock() 8 | submenu = RpiLCDSubMenu(base_menu_mock) 9 | assert isinstance(submenu, RpiLCDSubMenu) 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files=tests/unit/* 3 | addopts = -r fsxX 4 | 5 | [tox] 6 | envlist=py27,py34,py35,py36 7 | skip_missing_interpreters=True 8 | 9 | [testenv] 10 | commands=py.test {posargs} 11 | extras= stomp 12 | deps= 13 | mock 14 | pytest 15 | pytest-cov 16 | pytest-timeout 17 | passenv=TEST_STOMP_* 18 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/items/test_submenu_item.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from rpilcdmenu.items.submenu_item import SubmenuItem 3 | 4 | 5 | def test_submenuitem_action_starts_submenu(): 6 | submenu_mock = mock.Mock() 7 | submenu_mock.start = mock.Mock() 8 | submenu_item = SubmenuItem('submenu', submenu_mock, mock.Mock()) 9 | submenu_item.action() 10 | submenu_mock.start.assert_called_once() 11 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/items/test_message_item.py: -------------------------------------------------------------------------------- 1 | from mock import Mock, MagicMock, patch 2 | from rpilcdmenu.items.message_item import MessageItem 3 | 4 | 5 | @patch('rpilcdmenu.items.message_item.MessageView') 6 | def test_messageitem_action_starts_message_view(message_view_mock): 7 | message_view_instance = MagicMock() 8 | message_view_mock.return_value = message_view_instance 9 | message_item = MessageItem('a message', Mock(), Mock()) 10 | message_item.action() 11 | message_view_instance.start.assert_called_once() 12 | -------------------------------------------------------------------------------- /rpilcdmenu/items/submenu_item.py: -------------------------------------------------------------------------------- 1 | from .menu_item import MenuItem 2 | 3 | class SubmenuItem(MenuItem): 4 | """ 5 | A menu item to open a submenu 6 | """ 7 | 8 | def __init__(self, text, submenu, menu=None): 9 | """ 10 | :ivar BaseMenu self.submenu: The submenu to be opened when this item is selected 11 | """ 12 | super(SubmenuItem, self).__init__(text=text, menu=menu) 13 | 14 | self.submenu = submenu 15 | if menu: 16 | self.submenu.parent = menu 17 | 18 | def action(self): 19 | """ 20 | On Subitem click 21 | """ 22 | return self.submenu.start() 23 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/items/test_function_item.py: -------------------------------------------------------------------------------- 1 | from rpilcdmenu.items.function_item import FunctionItem 2 | 3 | 4 | def test_functionitem_action_calls_configured_function_with_regular_args_and_returns_its_result(): 5 | function_item = FunctionItem("Test Item", lambda x: x, [2]) 6 | action_result = function_item.action() 7 | assert 2 == action_result 8 | assert 2 == function_item.get_return() 9 | 10 | 11 | def test_functionitem_action_calls_configured_function_with_keyword_args_and_returns_its_result(): 12 | function_item = FunctionItem("Test Item", lambda x, y: (y, x), None, {'x': 2, 'y': 3}) 13 | action_result = function_item.action() 14 | assert (3, 2) == action_result 15 | assert (3, 2) == function_item.get_return() 16 | -------------------------------------------------------------------------------- /rpilcdmenu/items/message_item.py: -------------------------------------------------------------------------------- 1 | from .menu_item import MenuItem 2 | from rpilcdmenu.views import MessageView 3 | 4 | 5 | class MessageItem(MenuItem): 6 | """ 7 | A menu item to open a submenu 8 | """ 9 | 10 | def __init__(self, text, message, menu, scrollable=False): 11 | """ 12 | :ivar str text: Message to be shown on display 13 | :ivar RpiLCDMenu menu: The menu which this item belongs to 14 | :ivar bool scrollable: is scrolling allowed 15 | """ 16 | super(MessageItem, self).__init__(text, menu) 17 | 18 | self.view = MessageView(menu, message, scrollable) 19 | 20 | if menu: 21 | self.view.parent = menu 22 | 23 | def action(self): 24 | """ 25 | On MessageItem click 26 | """ 27 | return self.view.start() 28 | -------------------------------------------------------------------------------- /examples/example2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | single level menu 5 | """ 6 | 7 | from rpilcdmenu import * 8 | from rpilcdmenu.items import * 9 | 10 | def main(): 11 | menu = RpiLCDMenu(26,19,[13, 6, 5, 21]) 12 | function_item1 = FunctionItem("Item 1", fooMethod, [1]) 13 | function_item2 = FunctionItem("Item 2", fooMethod, [2]) 14 | function_item3 = FunctionItem("Item 3", fooMethod, [3]) 15 | function_item4 = FunctionItem("Item 4", fooMethod, [4]) 16 | menu.append_item(function_item1) 17 | menu.append_item(function_item2) 18 | menu.append_item(function_item3) 19 | menu.append_item(function_item4) 20 | menu.start() 21 | menu.processEnter() 22 | menu.processDown() 23 | menu.processDown() 24 | menu.processEnter() 25 | menu.processUp() 26 | menu.processEnter() 27 | 28 | def fooMethod(item_index): 29 | """ 30 | sample method with a parameter 31 | """ 32 | print("item %d pressed" % (item_index)) 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/helpers/test_text_helper.py: -------------------------------------------------------------------------------- 1 | from rpilcdmenu.helpers.text_helper import get_scrolled_line, get_scrolled_text, get_text_lines 2 | 3 | 4 | def test_get_scrolled_line_returns_requested_line_of_sixteen_characters_given_long_text(): 5 | sample_text = "Lorem ipsum dolor sit amet" 6 | result = get_scrolled_line(sample_text, 0) 7 | 8 | assert result == "Lorem ipsum dolo" 9 | 10 | 11 | def test_get_scrolled_line_returns_requested_line_of_characters_given_text_with_newlines(): 12 | 13 | result = get_scrolled_text("foo\nbar\nbaz", 2) 14 | 15 | assert result == "baz" 16 | 17 | 18 | def test_get_scrolled_text_returns_requested_lines_of_sixteen_characters_given_long_text(): 19 | sample_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et" 20 | result = get_scrolled_text(sample_text, 1) 21 | 22 | assert result == "r sit amet, consectetur adipisci" 23 | 24 | 25 | def test_get_text_lines_returns_how_many_lines_given_message_has(): 26 | 27 | result = get_text_lines("a\nb\ncccccccccccccccc") 28 | 29 | assert result == 3 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RPI LCD Menu - creating menu on 16x2 LCD with Raspberry PI 2 | [![Build Status](https://travis-ci.org/Dublerq/rpi-lcd-menu.svg?branch=master)](https://travis-ci.org/Dublerq/rpi-lcd-menu) 3 | [![codecov](https://codecov.io/gh/Dublerq/rpi-lcd-menu/branch/master/graph/badge.svg)](https://codecov.io/gh/Dublerq/rpi-lcd-menu) 4 | 5 | RPI LCD Menu is a python library for creating multi level menus displayed on 16x2 LCD screens (i.e. hd44780). 6 | Navigation can be easily implemented for any user input (buttons, joysticks, switches, detectors etc.). 7 | 8 | Tested on python 2.7 and 3.4+. 9 | 10 | # Demo 11 | ![Example in-use photo](/doc/rpi-example.jpg) 12 | 13 | Configuration used in examples: 14 | 15 | ![Configuration used in examples](/doc/configuration.png) 16 | 17 | # Code examples 18 | 19 | Sample snippets are stored in examples directory. Their content is as follows: 20 | * example.py - init screen and display test message to find out if everything is wired correctly 21 | * example2.py - create 1-level menu and test software navigation through entries 22 | * example3.py - create 2-level menu (menu with submenus) and test software navigation through entries 23 | * example4.py - example3.py with physical navigation using analog joystick and buttons (configuration as on image above) 24 | * example5.py - scrollable message view with physical navigation as in example4 25 | -------------------------------------------------------------------------------- /rpilcdmenu/items/function_item.py: -------------------------------------------------------------------------------- 1 | from .menu_item import MenuItem 2 | 3 | 4 | class FunctionItem(MenuItem): 5 | """ 6 | A menu item to call a Python function 7 | """ 8 | 9 | def __init__(self, text, function, args=None, kwargs=None, menu=None): 10 | """ 11 | :ivar function: The function to be called 12 | :ivar list args: An optional list of arguments to be passed to the function 13 | :ivar dict kwargs: An optional dictionary of keyword arguments to be passed to the function 14 | :ivar RpiLCDMenu menu: The menu which this item belongs to 15 | """ 16 | super(FunctionItem, self).__init__(text=text, menu=menu) 17 | 18 | self.function = function 19 | 20 | if args is not None: 21 | self.args = args 22 | else: 23 | self.args = [] 24 | if kwargs is not None: 25 | self.kwargs = kwargs 26 | else: 27 | self.kwargs = {} 28 | 29 | self.returned_value = None 30 | 31 | def action(self): 32 | """ 33 | This class overrides this method 34 | """ 35 | self.returned_value = self.function(*self.args, **self.kwargs) 36 | return self.returned_value 37 | 38 | def get_return(self): 39 | """ 40 | :return: The return value from the function call 41 | """ 42 | return self.returned_value 43 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/items/test_menu_item.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import mock 3 | from rpilcdmenu.items.menu_item import MenuItem 4 | 5 | 6 | def test_menuitem_throws_exception_when_title_is_too_long(): 7 | with pytest.raises(NameError): 8 | MenuItem("Definetely Too Long Text To Display") 9 | 10 | 11 | def test_menuitem_can_return_its_title_as_string(): 12 | menu_item = MenuItem("an Item") 13 | assert "an Item" == menu_item.__str__() 14 | 15 | 16 | def test_menuitem_show_return_menu_representaiton(): 17 | menu_item = MenuItem("an Item") 18 | assert "3 - an Item" == menu_item.show(2) 19 | 20 | 21 | def test_menuitem_has_set_up_method(): 22 | menu_item = MenuItem("an Item") 23 | assert None == menu_item.set_up() 24 | 25 | 26 | def test_menuitem_has_action_method(): 27 | menu_item = MenuItem("an Item") 28 | assert None == menu_item.action() 29 | 30 | 31 | def test_menuitem_has_cleanup_method(): 32 | menu_item = MenuItem("an Item") 33 | assert None == menu_item.clean_up() 34 | 35 | 36 | def test_menuitem_get_return_returns_parent_menu_value_given_parent_menu(): 37 | parent_menu_mock = mock.Mock() 38 | parent_menu_mock.get_return.return_value=123 39 | 40 | menu_item = MenuItem("an Item", parent_menu_mock) 41 | 42 | assert 123 == menu_item.get_return() 43 | 44 | 45 | def test_menuitem_get_return_returns_None_given_no_parent_menu(): 46 | menu_item = MenuItem("an Item") 47 | assert None == menu_item.get_return() 48 | -------------------------------------------------------------------------------- /rpilcdmenu/helpers/text_helper.py: -------------------------------------------------------------------------------- 1 | 2 | def get_scrolled_line(text, line_number=0): 3 | """ 4 | :param str text: message to be scrolled 5 | :param int line_number: which number to start from 6 | """ 7 | scrolled_text = '' 8 | char_index = 0 9 | line_index = 0 10 | 11 | for char in text: 12 | if line_index == line_number: 13 | scrolled_text += char 14 | 15 | char_index += 1 16 | 17 | if char_index == 16 or char == '\n': 18 | if line_index == line_number: 19 | return scrolled_text 20 | char_index = 0 21 | line_index += 1 22 | 23 | return scrolled_text 24 | 25 | 26 | def get_scrolled_text(text, start_line=0, lines_required=2): 27 | """ 28 | :param str text: message to be scrolled 29 | :param int start_line: which number to start from 30 | :param int lines_required: how many lines are needed 31 | """ 32 | scrolled_text = '' 33 | 34 | for line in range(start_line, start_line+lines_required): 35 | scrolled_text += get_scrolled_line(text, line) 36 | 37 | return scrolled_text 38 | 39 | 40 | def get_text_lines(text): 41 | """ 42 | :param str text: message to evaluate 43 | :return int: how many lines the message has 44 | """ 45 | char_index = 0 46 | line_counter = 1 47 | 48 | for char in text: 49 | if char_index == 17 or char == '\n': 50 | char_index = 0 51 | line_counter += 1 52 | char_index += 1 53 | 54 | return line_counter 55 | -------------------------------------------------------------------------------- /examples/example3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | multi level menu 5 | """ 6 | 7 | from rpilcdmenu import * 8 | from rpilcdmenu.items import * 9 | 10 | 11 | def main(): 12 | menu = RpiLCDMenu(26, 19, [13, 6, 5, 21]) 13 | 14 | function_item1 = FunctionItem("Item 1", fooFunction, [1]) 15 | function_item2 = FunctionItem("Item 2", fooFunction, [2]) 16 | menu.append_item(function_item1).append_item(function_item2) 17 | 18 | submenu = RpiLCDSubMenu(menu) 19 | submenu_item = SubmenuItem("SubMenu (3)", submenu, menu) 20 | menu.append_item(submenu_item) 21 | 22 | submenu.append_item(FunctionItem("Item 31", fooFunction, [31])).append_item( 23 | FunctionItem("Item 32", fooFunction, [32])) 24 | submenu.append_item(FunctionItem("Back", exitSubMenu, [submenu])) 25 | 26 | menu.append_item(FunctionItem("Item 4", fooFunction, [4])) 27 | 28 | menu.start() 29 | menu.debug() 30 | print("----") 31 | # press first menu item and scroll down to third one 32 | menu.processEnter().processDown().processDown() 33 | # enter submenu, press Item 32, press Back button 34 | menu.processEnter().processDown().processEnter().processDown().processEnter() 35 | # press item4 back in the menu 36 | menu.processDown().processEnter() 37 | 38 | 39 | def fooFunction(item_index): 40 | """ 41 | sample method with a parameter 42 | """ 43 | print("item %d pressed" % (item_index)) 44 | 45 | 46 | def exitSubMenu(submenu): 47 | return submenu.exit() 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /rpilcdmenu/views/message_view.py: -------------------------------------------------------------------------------- 1 | from rpilcdmenu.rpi_lcd_submenu import RpiLCDSubMenu 2 | from rpilcdmenu.helpers.text_helper import get_scrolled_text, get_text_lines 3 | 4 | 5 | class MessageView(RpiLCDSubMenu): 6 | def __init__(self, base_menu, text, scrollable=False): 7 | """ 8 | Initialize MessageView 9 | :ivar RpiLCDMenu base_menu: The menu which this item belongs to 10 | :ivar str text: Message to be shown on display 11 | :ivar bool scrollable: is scrolling allowed 12 | """ 13 | 14 | self.scrollable = scrollable 15 | self.line_index = 0 16 | self.text_lines = 0 17 | self.text = '' 18 | 19 | self.setText(text) 20 | 21 | super(MessageView, self).__init__(base_menu) 22 | 23 | def render(self): 24 | """ 25 | Render menu 26 | """ 27 | self.clearDisplay() 28 | 29 | if self.scrollable: 30 | self.message(get_scrolled_text(self.text, self.line_index)) 31 | else: 32 | self.message(self.text) 33 | 34 | return self 35 | 36 | def processUp(self): 37 | if self.line_index > 0 and self.scrollable: 38 | self.line_index -= 1 39 | self.render() 40 | 41 | return self 42 | 43 | def processDown(self): 44 | if self.line_index < self.text_lines - 1 and self.scrollable: 45 | self.line_index += 1 46 | self.render() 47 | 48 | return self 49 | 50 | def processEnter(self): 51 | return self.exit() 52 | 53 | def setText(self, text): 54 | self.text = text 55 | self.text_lines = get_text_lines(text) 56 | -------------------------------------------------------------------------------- /examples/example5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | menu with message view and physical steering 5 | """ 6 | 7 | from rpilcdmenu import * 8 | from rpilcdmenu.items import * 9 | import RPi.GPIO as GPIO 10 | import smbus 11 | import time 12 | 13 | 14 | def main(): 15 | # configure standard button 16 | GPIO.setmode(GPIO.BCM) 17 | GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) 18 | prev_button = 0 19 | 20 | # create menu as in example3 21 | menu = RpiLCDMenu(26, 19, [13, 6, 5, 21]) 22 | 23 | menu.append_item( 24 | MessageItem('message item', 25 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut ' 26 | 'labore et dolore magna aliqua.', 27 | menu, 28 | True) 29 | ) 30 | 31 | menu.start() 32 | 33 | # configure physical analog joystick via adc converter over i2c 34 | address = 0x48 35 | a0 = 0x40 # output of y axis 36 | bus = smbus.SMBus(1) 37 | 38 | while True: 39 | bus.write_byte(address, a0) 40 | y = bus.read_byte(address) * 3.3 / 255 41 | 42 | if y > 2.5: 43 | menu = menu.processUp() 44 | time.sleep(0.25) 45 | elif y < 0.7: 46 | menu = menu.processDown() 47 | time.sleep(0.25) 48 | 49 | # physical button 50 | button = GPIO.input(27) 51 | if prev_button == 0 and button != 0: 52 | menu = menu.processEnter() 53 | 54 | prev_button = button 55 | 56 | time.sleep(0.25) 57 | 58 | 59 | def exit_sub_menu(submenu): 60 | return submenu.exit() 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /rpilcdmenu/items/menu_item.py: -------------------------------------------------------------------------------- 1 | class MenuItem(object): 2 | """ 3 | A generic menu item 4 | """ 5 | 6 | def __init__(self, text, menu=None): 7 | """ 8 | :ivar str text: The text shown for this menu item 9 | :ivar RpiLCDMenu menu: The menu which this item belongs to 10 | """ 11 | if len(text) > 15 or len(text) == 0: 12 | raise NameError('MenuTextTooLong'); 13 | self.text = text 14 | self.menu = menu 15 | 16 | def __str__(self): 17 | return "%s" % self.text 18 | 19 | def show(self, index): 20 | """ 21 | How this item should be displayed in the menu. Can be overridden, but should keep the same pattern 22 | Default is: 23 | 1 - Item 1 24 | 2 - Another Item 25 | :param int index: The index of the item in the items list of the menu 26 | :return: The representation of the item to be shown in a menu 27 | :rtype: str 28 | """ 29 | return "%d - %s" % (index + 1, self.text) 30 | 31 | def set_up(self): 32 | """ 33 | Override to add any setup actions necessary for the item 34 | """ 35 | pass 36 | 37 | def action(self): 38 | """ 39 | Override to carry out the main action for this item. 40 | """ 41 | pass 42 | 43 | def clean_up(self): 44 | """ 45 | Override to add any cleanup actions necessary for the item 46 | """ 47 | pass 48 | 49 | def get_return(self): 50 | """ 51 | Override to change what the item returns. 52 | Otherwise just returns the same value the last selected item did. 53 | """ 54 | return self.menu is not None and self.menu.get_return() or None 55 | -------------------------------------------------------------------------------- /examples/example4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | multi level menu with physical steering 5 | """ 6 | 7 | from rpilcdmenu import * 8 | from rpilcdmenu.items import * 9 | import RPi.GPIO as GPIO 10 | import smbus 11 | import time 12 | 13 | def main(): 14 | #configure standard button 15 | GPIO.setmode(GPIO.BCM) 16 | GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) 17 | prev_button = 0 18 | 19 | #create menu as in example3 20 | menu = RpiLCDMenu(26,19,[13, 6, 5, 21]) 21 | 22 | function_item1 = FunctionItem("Item 1", fooFunction, [1]) 23 | function_item2 = FunctionItem("Item 2", fooFunction, [2]) 24 | menu.append_item(function_item1).append_item(function_item2) 25 | 26 | submenu = RpiLCDSubMenu(menu) 27 | submenu_item = SubmenuItem("SubMenu (3)", submenu, menu) 28 | menu.append_item(submenu_item) 29 | 30 | submenu.append_item( FunctionItem("Item 31", fooFunction, [31])).append_item( FunctionItem("Item 32", fooFunction, [32])) 31 | submenu.append_item( FunctionItem("Back", exitSubMenu, [submenu])) 32 | 33 | menu.append_item(FunctionItem("Item 4", fooFunction, [4])) 34 | 35 | menu.start() 36 | 37 | #configure physical analog joystick via adc converter over i2c 38 | address = 0x48 39 | A0 = 0x40 40 | A1 = 0x41 41 | A2 = 0x42 42 | A3 = 0x43 43 | bus = smbus.SMBus(1) 44 | while True: 45 | bus.write_byte(address,A0) 46 | x = bus.read_byte(address)*3.3/255 47 | bus.write_byte(address,A1) 48 | y = bus.read_byte(address)*3.3/255 49 | if (y > 2.5): 50 | menu = menu.processUp() 51 | time.sleep(0.5) 52 | elif (y < 0.7): 53 | menu = menu.processDown() 54 | time.sleep(0.5) 55 | 56 | #physical button 57 | button = GPIO.input(27) 58 | if (prev_button == 0 and button != 0): 59 | menu = menu.processEnter() 60 | prev_button = button 61 | 62 | time.sleep(0.25) 63 | 64 | def fooFunction(item_index): 65 | """ 66 | sample method with a parameter 67 | """ 68 | print("item %d pressed" % (item_index)) 69 | 70 | def exitSubMenu(submenu): 71 | return submenu.exit() 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/test_base_menu.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from rpilcdmenu.base_menu import BaseMenu 3 | 4 | def test_basemenu_can_be_initialized_entered_and_exited(): 5 | base_menu = BaseMenu(mock.Mock()) 6 | base_menu.start() 7 | base_menu.exit() 8 | 9 | 10 | def test_basemenu_can_append_scroll_and_select_menuitems(): 11 | base_menu = BaseMenu() 12 | base_menu.start() 13 | 14 | menuitem_mock = mock.Mock() 15 | target_menuitem_mock = mock.Mock() 16 | target_menuitem_mock.action = mock.Mock() 17 | 18 | base_menu.append_item(menuitem_mock) 19 | base_menu.append_item(target_menuitem_mock) 20 | 21 | base_menu.processDown() 22 | base_menu.processDown() 23 | base_menu.processUp() 24 | base_menu.processUp() 25 | base_menu.processDown() 26 | 27 | base_menu.processEnter() 28 | 29 | target_menuitem_mock.action.assert_called_once() 30 | 31 | 32 | def test_basemenu_process_process_enter_renders_submenu_when_submenu_item_selected(): 33 | base_menu = BaseMenu() 34 | base_menu.start() 35 | 36 | submenu_mock = mock.Mock() 37 | submenu_mock.__class__ = BaseMenu 38 | 39 | menuitem_mock = mock.Mock() 40 | menuitem_mock.action = mock.Mock() 41 | menuitem_mock.action.return_value = submenu_mock 42 | 43 | base_menu.append_item(menuitem_mock) 44 | 45 | assert submenu_mock == base_menu.processEnter() 46 | 47 | 48 | def test_basemenu_clearDisplay_exists(): 49 | base_menu = BaseMenu(mock.Mock()) 50 | base_menu.clearDisplay() 51 | 52 | 53 | def test_basemenu_debug_returns_subitem_debug_info(): 54 | base_menu = BaseMenu() 55 | base_menu.start() 56 | 57 | menuitem_mock = mock.Mock() 58 | submenuitem_mock = mock.Mock() 59 | submenuitem_mock.submenu = mock.Mock() 60 | submenuitem_mock.submenu.debug = mock.Mock() 61 | submenuitem_mock.submenu.__class__ = BaseMenu 62 | base_menu.append_item(menuitem_mock) 63 | base_menu.append_item(submenuitem_mock) 64 | 65 | base_menu.debug() 66 | submenuitem_mock.submenu.debug.assert_called_once() 67 | -------------------------------------------------------------------------------- /rpilcdmenu/rpi_lcd_menu.py: -------------------------------------------------------------------------------- 1 | from rpilcdmenu.base_menu import BaseMenu 2 | from rpilcdmenu.rpi_lcd_hwd import RpiLCDHwd 3 | 4 | 5 | class RpiLCDMenu(BaseMenu): 6 | def __init__(self, pin_rs=26, pin_e=19, pins_db=[13, 6, 5, 21], GPIO=None): 7 | """ 8 | Initialize menu 9 | """ 10 | 11 | self.lcd = RpiLCDHwd(pin_rs, pin_e, pins_db, GPIO) 12 | 13 | self.lcd.initDisplay() 14 | self.clearDisplay() 15 | 16 | super(self.__class__, self).__init__() 17 | 18 | def clearDisplay(self): 19 | """ 20 | Clear LCD Screen 21 | """ 22 | self.lcd.write4bits(RpiLCDHwd.LCD_CLEARDISPLAY) # command to clear display 23 | self.lcd.delayMicroseconds(3000) # 3000 microsecond sleep, clearing the display takes a long time 24 | 25 | return self 26 | 27 | def message(self, text): 28 | """ Send long string to LCD. 17th char wraps to second line""" 29 | i = 0 30 | lines = 0 31 | 32 | for char in text: 33 | if char == '\n': 34 | self.lcd.write4bits(0xC0) # next line 35 | i = 0 36 | lines += 1 37 | else: 38 | self.lcd.write4bits(ord(char), True) 39 | i = i + 1 40 | 41 | if i == 16: 42 | self.lcd.write4bits(0xC0) # last char of the line 43 | elif lines == 2: 44 | break 45 | 46 | return self 47 | 48 | def displayTestScreen(self): 49 | """ 50 | Display test screen to see if your LCD screen is wokring 51 | """ 52 | self.message('Hum. body 36,6\xDFC\nThis is test') 53 | 54 | return self 55 | 56 | def render(self): 57 | """ 58 | Render menu 59 | """ 60 | self.clearDisplay() 61 | 62 | if len(self.items) == 0: 63 | self.message('Menu is empty') 64 | return self 65 | elif len(self.items) <= 2: 66 | options = (self.current_option == 0 and ">" or " ") + self.items[0].text 67 | if len(self.items) == 2: 68 | options += "\n" + (self.current_option == 1 and ">" or " ") + self.items[1].text 69 | print(options) 70 | self.message(options) 71 | return self 72 | 73 | options = ">" + self.items[self.current_option].text 74 | 75 | if self.current_option + 1 < len(self.items): 76 | options += "\n " + self.items[self.current_option + 1].text 77 | else: 78 | options += "\n " + self.items[0].text 79 | 80 | self.message(options) 81 | 82 | return self 83 | -------------------------------------------------------------------------------- /rpilcdmenu/base_menu.py: -------------------------------------------------------------------------------- 1 | class BaseMenu(object): 2 | """ 3 | A generic menu 4 | """ 5 | def __init__(self, parent=None): 6 | """ 7 | Initialzie basic menu 8 | """ 9 | self.items = list() 10 | self.parent = parent 11 | self.current_option = 0 12 | self.selected_option = -1 13 | 14 | def start(self): 15 | """ 16 | Start and render menu 17 | """ 18 | self.current_option = 0 19 | self.selected_option = -1 20 | self.render() 21 | 22 | return self 23 | 24 | def debug(self, level=1): 25 | """ 26 | print menu items in console 27 | """ 28 | for item in self.items: 29 | if hasattr(item, 'submenu') and isinstance(item.submenu, BaseMenu): 30 | print("|" + "--" * (level + 1) + "[" + "%s" % (item.__str__()) + "]") 31 | item.submenu.debug(level+1) 32 | else: 33 | print("|" + "--" * level + ">" + "%s" % (item.__str__())) 34 | return self 35 | 36 | def append_item(self, item): 37 | """ 38 | Add an item to the end of the menu 39 | :param MenuItem item: The item to be added 40 | """ 41 | item.menu = self 42 | self.items.append(item) 43 | return self 44 | 45 | def render(self): 46 | """ 47 | Render menu 48 | """ 49 | pass 50 | 51 | def clearDisplay(self): 52 | """ 53 | Clear the screen/ 54 | """ 55 | pass 56 | 57 | def processUp(self): 58 | """ 59 | User triggered up event 60 | """ 61 | if self.current_option == 0: 62 | self.current_option = len(self.items) - 1 63 | else: 64 | self.current_option -= 1 65 | self.render() 66 | return self 67 | 68 | def processDown(self): 69 | """ 70 | User triggered down event 71 | """ 72 | if self.current_option == len(self.items) - 1: 73 | self.current_option = 0 74 | else: 75 | self.current_option += 1 76 | self.render() 77 | return self 78 | 79 | def processEnter(self): 80 | """ 81 | User triggered enter event 82 | """ 83 | action_result = self.items[self.current_option].action() 84 | if isinstance(action_result, BaseMenu): 85 | return action_result 86 | return self 87 | 88 | def exit(self): 89 | """ 90 | exit submenu and return parent 91 | """ 92 | if self.parent is not None: 93 | self.parent.render() 94 | return self.parent 95 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/views/test_message_view.py: -------------------------------------------------------------------------------- 1 | from mock import call, Mock, MagicMock, patch 2 | from rpilcdmenu.views.message_view import MessageView 3 | 4 | 5 | @patch('rpilcdmenu.views.message_view.get_text_lines') 6 | @patch('rpilcdmenu.views.message_view.get_scrolled_text') 7 | def test_messageview_render_shows_full_message_in_non_scrollable_mode(get_scrolled_text, get_text_lines): 8 | message_view = MessageView(Mock(), 'Some multi-line\ntext to be shown\n on LCD', False) 9 | 10 | message_view.message = Mock() 11 | message_view.render() 12 | message_view.message.assert_called_once_with('Some multi-line\ntext to be shown\n on LCD') 13 | 14 | 15 | @patch('rpilcdmenu.views.message_view.get_text_lines') 16 | @patch('rpilcdmenu.views.message_view.get_scrolled_text') 17 | def test_messageview_render_shows_only_part_of_text_in_scrollable_mode(get_scrolled_text, get_text_lines): 18 | get_scrolled_text.return_value = 'Some multi-line\ntext to be shown' 19 | get_text_lines.return_value = 3 20 | 21 | message_view = MessageView(Mock(), 'Some multi-line\ntext to be shown\n on LCD', True) 22 | 23 | message_view.message = Mock() 24 | message_view.render() 25 | message_view.message.assert_called_once_with('Some multi-line\ntext to be shown') 26 | 27 | 28 | @patch('rpilcdmenu.views.message_view.get_text_lines') 29 | @patch('rpilcdmenu.views.message_view.get_scrolled_text') 30 | def test_messageview_processDown_scrolls_down_given_message(get_scrolled_text, get_text_lines): 31 | get_scrolled_text.return_value = ' on LCD' 32 | get_text_lines.return_value = 3 33 | 34 | message_view = MessageView(Mock(), 'Some multi-line\ntext to be shown\n on LCD', True) 35 | 36 | message_view.message = Mock() 37 | 38 | message_view.render() 39 | message_view.processDown() 40 | message_view.processDown() 41 | message_view.processDown() 42 | message_view.processDown() 43 | 44 | assert message_view.message.mock_calls[-1] == call(' on LCD') 45 | 46 | 47 | @patch('rpilcdmenu.views.message_view.get_text_lines') 48 | @patch('rpilcdmenu.views.message_view.get_scrolled_text') 49 | def test_messageview_processDown_scrolls_given_message_up_after_scrolling_it_down(get_scrolled_text, get_text_lines): 50 | get_scrolled_text.return_value = 'Some multi-line\ntext to be shown' 51 | get_text_lines.return_value = 3 52 | 53 | message_view = MessageView(Mock(), 'Some multi-line\ntext to be shown\n on LCD', True) 54 | 55 | message_view.message = Mock() 56 | 57 | message_view.render() 58 | message_view.processDown() 59 | message_view.processDown() 60 | message_view.processUp() 61 | message_view.processUp() 62 | message_view.processUp() 63 | 64 | assert message_view.message.mock_calls[-1] == call('Some multi-line\ntext to be shown') 65 | 66 | 67 | @patch('rpilcdmenu.views.message_view.get_text_lines') 68 | @patch('rpilcdmenu.views.message_view.get_scrolled_text') 69 | def test_messageview_processEnter_exits_to_parent_menu(get_scrolled_text, get_text_lines): 70 | parent_menu_mock = MagicMock() 71 | 72 | message_view = MessageView(parent_menu_mock, 'Some multi-line\ntext to be shown\n on LCD', True) 73 | message_view.processEnter() 74 | parent_menu_mock.render.assert_called_once() 75 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/test_rpi_lcd_hwd.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | import datetime 4 | from mock import Mock, MagicMock, patch, call 5 | 6 | from rpilcdmenu.rpi_lcd_hwd import RpiLCDHwd 7 | 8 | 9 | def test_rpilcdhwd_cannot_be_initialized_without_gpio_support(): 10 | with patch.dict(sys.modules, {'RPi.GPIO': None}): 11 | with pytest.raises(ImportError): 12 | RpiLCDHwd() 13 | 14 | 15 | def test_rpilcdhwd_imports_gpio_and_initializes_provided_gpio_pins_in_bcm_mode(): 16 | GPIO_mock = Mock() 17 | GPIO_mock.setup = Mock() 18 | GPIO_mock.OUT = 'out' 19 | GPIO_mock.IN = 'in' 20 | GPIO_mock.BCM = 'BCM' 21 | GPIO_mock.setmode = Mock() 22 | RPi_mock = Mock() 23 | RPi_mock.GPIO = GPIO_mock 24 | 25 | with patch.dict(sys.modules, {'RPi': RPi_mock, 'RPi.GPIO': Mock()}): 26 | RpiLCDHwd(1, 2, [3, 4, 5, 6]) 27 | 28 | GPIO_mock.setmode.assert_called_once_with(GPIO_mock.BCM) 29 | 30 | setup_calls = [ 31 | call(1, GPIO_mock.OUT), 32 | call(2, GPIO_mock.OUT), 33 | call(3, GPIO_mock.OUT), 34 | call(4, GPIO_mock.OUT), 35 | call(5, GPIO_mock.OUT), 36 | call(6, GPIO_mock.OUT) 37 | ] 38 | 39 | GPIO_mock.setup.assert_has_calls(setup_calls, any_order=True) 40 | 41 | 42 | def test_rpilcdhwd_initDisplay_configures_proper_lcd_settings(): 43 | RPi_mock = Mock() 44 | RPi_mock.GPIO = MagicMock() 45 | 46 | with patch.dict(sys.modules, {'RPi': RPi_mock, 'RPi.GPIO': Mock()}): 47 | lcd = RpiLCDHwd(1, 2, [3, 4, 5, 6]) 48 | 49 | lcd.write4bits = Mock() 50 | lcd.initDisplay() 51 | 52 | assert lcd.write4bits.mock_calls == [ 53 | call(0x33), 54 | call(0x32), 55 | call(0x28), 56 | call(0x0C), 57 | call(0x06), 58 | call(0x06), 59 | ] 60 | 61 | 62 | def test_rpilcdhwd_initDisplay_configures_proper_lcd_settings(): 63 | RPi_mock = Mock() 64 | RPi_mock.GPIO = MagicMock() 65 | 66 | with patch.dict(sys.modules, {'RPi': RPi_mock, 'RPi.GPIO': Mock()}): 67 | lcd = RpiLCDHwd(1, 2, [3, 4, 5, 6]) 68 | 69 | lcd.write4bits = Mock() 70 | lcd.initDisplay() 71 | 72 | assert lcd.write4bits.mock_calls == [ 73 | call(0x33), 74 | call(0x32), 75 | call(0x28), 76 | call(0x0C), 77 | call(0x06), 78 | call(0x06), 79 | ] 80 | 81 | 82 | def test_rpilcdmenu_write4bits_transfers_data_through_GPIO(): 83 | RPi_mock = Mock() 84 | RPi_mock.GPIO = Mock() 85 | RPi_mock.GPIO.output = Mock() 86 | 87 | with patch.dict(sys.modules, {'RPi': RPi_mock, 'RPi.GPIO': Mock()}): 88 | lcd = RpiLCDHwd(1, 2, [3, 4, 5, 6]) 89 | 90 | lcd.delayMicroseconds = Mock() 91 | lcd.pulseEnable = Mock() 92 | 93 | lcd.write4bits(0x123) 94 | assert RPi_mock.GPIO.output.mock_calls == [ 95 | call(1, False), 96 | call(3, False), 97 | call(4, False), 98 | call(5, False), 99 | call(6, False), 100 | call(6, True), 101 | call(3, True), 102 | call(3, False), 103 | call(4, False), 104 | call(5, False), 105 | call(6, False), 106 | call(3, True) 107 | ] 108 | 109 | 110 | def test_rpilcdmenu_delayMicroseconds_waits_given_microseconds(): 111 | RPi_mock = Mock() 112 | RPi_mock.GPIO = Mock() 113 | 114 | with patch.dict(sys.modules, {'RPi': RPi_mock, 'RPi.GPIO': Mock()}): 115 | lcd = RpiLCDHwd(1, 2, [3, 4, 5, 6]) 116 | 117 | start_time = datetime.datetime.now() 118 | 119 | lcd.delayMicroseconds(10) 120 | 121 | assert (datetime.datetime.now() - start_time).microseconds >= 10 122 | 123 | 124 | def test_rpilcdmenu_pulseEnable_is_blinking_pin_e(): 125 | RPi_mock = Mock() 126 | RPi_mock.GPIO = Mock() 127 | RPi_mock.GPIO.output = Mock() 128 | 129 | with patch.dict(sys.modules, {'RPi': RPi_mock, 'RPi.GPIO': Mock()}): 130 | lcd = RpiLCDHwd(1, 2, [3, 4, 5, 6]) 131 | 132 | lcd.pulseEnable() 133 | 134 | assert RPi_mock.GPIO.output.mock_calls == [call(2, False), call(2, True), call(2, False)] 135 | -------------------------------------------------------------------------------- /rpilcdmenu/rpi_lcd_hwd.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | 4 | class RpiLCDHwd: 5 | 6 | # commands 7 | LCD_CLEARDISPLAY = 0x01 8 | LCD_RETURNHOME = 0x02 9 | LCD_ENTRYMODESET = 0x04 10 | LCD_DISPLAYCONTROL = 0x08 11 | LCD_CURSORSHIFT = 0x10 12 | LCD_FUNCTIONSET = 0x20 13 | LCD_SETCGRAMADDR = 0x40 14 | LCD_SETDDRAMADDR = 0x80 15 | 16 | # flags for display entry mode 17 | LCD_ENTRYRIGHT = 0x00 18 | LCD_ENTRYLEFT = 0x02 19 | LCD_ENTRYSHIFTINCREMENT = 0x01 20 | LCD_ENTRYSHIFTDECREMENT = 0x00 21 | 22 | # flags for display on/off control 23 | LCD_DISPLAYON = 0x04 24 | LCD_DISPLAYOFF = 0x00 25 | LCD_CURSORON = 0x02 26 | LCD_CURSOROFF = 0x00 27 | LCD_BLINKON = 0x01 28 | LCD_BLINKOFF = 0x00 29 | 30 | # flags for display/cursor shift 31 | LCD_DISPLAYMOVE = 0x08 32 | LCD_CURSORMOVE = 0x00 33 | 34 | # flags for display/cursor shift 35 | LCD_DISPLAYMOVE = 0x08 36 | LCD_CURSORMOVE = 0x00 37 | LCD_MOVERIGHT = 0x04 38 | LCD_MOVELEFT = 0x00 39 | 40 | # flags for function set 41 | LCD_8BITMODE = 0x10 42 | LCD_4BITMODE = 0x00 43 | LCD_2LINE = 0x08 44 | LCD_1LINE = 0x00 45 | LCD_5x10DOTS = 0x04 46 | LCD_5x8DOTS = 0x00 47 | 48 | def __init__(self, pin_rs=26, pin_e=19, pins_db=[13, 6, 5, 21], GPIO=None): 49 | """ 50 | LCD GPIO configuration 51 | """ 52 | if not GPIO: 53 | import RPi.GPIO as GPIO 54 | GPIO.setwarnings(False) 55 | 56 | self.GPIO = GPIO 57 | self.pin_rs = pin_rs 58 | self.pin_e = pin_e 59 | self.pins_db = pins_db 60 | 61 | self.displaycontrol = None 62 | self.displayfunction = None 63 | self.displaymode = None 64 | 65 | self.GPIO.setmode(GPIO.BCM) 66 | self.GPIO.setup(self.pin_rs, GPIO.OUT) 67 | self.GPIO.setup(self.pin_e, GPIO.OUT) 68 | 69 | for pin in self.pins_db: 70 | self.GPIO.setup(pin, GPIO.OUT) 71 | 72 | def initDisplay(self): 73 | self.write4bits(0x33) # initialization 74 | self.write4bits(0x32) # initialization 75 | self.write4bits(0x28) # 2 line 5x7 matrix 76 | self.write4bits(0x0C) # turn cursor off 0x0E to enable cursor 77 | self.write4bits(0x06) # shift cursor right 78 | 79 | self.displaycontrol = self.LCD_DISPLAYON | self.LCD_CURSOROFF | self.LCD_BLINKOFF 80 | 81 | self.displayfunction = self.LCD_4BITMODE | self.LCD_1LINE | self.LCD_5x8DOTS 82 | self.displayfunction |= self.LCD_2LINE 83 | 84 | # Initialize to default text direction (for romance languages) 85 | self.displaymode = self.LCD_ENTRYLEFT | self.LCD_ENTRYSHIFTDECREMENT 86 | self.write4bits(self.LCD_ENTRYMODESET | self.displaymode) # set the entry mode 87 | 88 | return self 89 | 90 | def write4bits(self, bits, char_mode=False): 91 | """ Send command to LCD """ 92 | self.delayMicroseconds(1000) # 1000 microsecond sleep 93 | bits = bin(bits)[2:].zfill(8) 94 | self.GPIO.output(self.pin_rs, char_mode) 95 | for pin in self.pins_db: 96 | self.GPIO.output(pin, False) 97 | 98 | for i in range(4): 99 | if bits[i] == "1": 100 | self.GPIO.output(self.pins_db[::-1][i], True) 101 | 102 | self.pulseEnable() 103 | 104 | for pin in self.pins_db: 105 | self.GPIO.output(pin, False) 106 | 107 | for i in range(4, 8): 108 | if bits[i] == "1": 109 | self.GPIO.output(self.pins_db[::-1][i - 4], True) 110 | 111 | self.pulseEnable() 112 | 113 | return self 114 | 115 | def delayMicroseconds(self, microseconds): 116 | seconds = microseconds / float(1000000) # divide microseconds by 1 million for seconds 117 | sleep(seconds) 118 | 119 | return self 120 | 121 | def pulseEnable(self): 122 | self.GPIO.output(self.pin_e, False) 123 | self.delayMicroseconds(1) # 1 microsecond pause - enable pulse must be > 450ns 124 | self.GPIO.output(self.pin_e, True) 125 | self.delayMicroseconds(1) # 1 microsecond pause - enable pulse must be > 450ns 126 | self.GPIO.output(self.pin_e, False) 127 | self.delayMicroseconds(1) # commands need > 37us to settle 128 | 129 | return self 130 | -------------------------------------------------------------------------------- /tests/unit/rpilcdmenu/test_rpi_lcd_menu.py: -------------------------------------------------------------------------------- 1 | from mock import Mock, MagicMock, patch, call 2 | from rpilcdmenu.rpi_lcd_menu import RpiLCDMenu 3 | 4 | 5 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 6 | def test_rpilcdmenu_imports_gpio_and_initializes_with_clear_screen(LCDHwdMock): 7 | LCDHwdMockInstance = MagicMock() 8 | LCDHwdMock.return_value = LCDHwdMockInstance 9 | 10 | GPIOMock = Mock() 11 | RpiLCDMenu(1, 2, [3, 4, 5, 6], GPIOMock) 12 | 13 | LCDHwdMock.assert_called_once_with(1, 2, [3, 4, 5, 6], GPIOMock) 14 | LCDHwdMockInstance.initDisplay.assert_called_once() 15 | LCDHwdMockInstance.write4bits.assert_called_once_with(LCDHwdMock.LCD_CLEARDISPLAY) 16 | LCDHwdMockInstance.delayMicroseconds.assert_called_once_with(3000) 17 | 18 | 19 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 20 | def test_rpilcdmenu_message_sends_bytes_of_message_to_rpi(LCDHwdMock): 21 | LCDHwdMockInstance = MagicMock() 22 | LCDHwdMock.return_value = LCDHwdMockInstance 23 | 24 | menu = RpiLCDMenu() 25 | LCDHwdMockInstance.reset_mock() 26 | 27 | menu.message("1\n") 28 | 29 | assert LCDHwdMockInstance.write4bits.mock_calls == [call(ord("1"), True), call(0xC0)] 30 | 31 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 32 | def test_rpilcdmenu_message_breaks_line_after_16_chars(LCDHwdMock): 33 | LCDHwdMockInstance = MagicMock() 34 | LCDHwdMock.return_value = LCDHwdMockInstance 35 | 36 | menu = RpiLCDMenu() 37 | LCDHwdMockInstance.reset_mock() 38 | 39 | menu.message("11111111111111112") 40 | 41 | assert LCDHwdMockInstance.write4bits.mock_calls == [ 42 | call(ord("1"), True) for i in range(16) 43 | ] + [call(0xC0), call(ord("2"), True)] 44 | 45 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 46 | def test_rpilcdmenu_message_is_trimmed_to_two_lines(LCDHwdMock): 47 | LCDHwdMockInstance = MagicMock() 48 | LCDHwdMock.return_value = LCDHwdMockInstance 49 | 50 | menu = RpiLCDMenu() 51 | LCDHwdMockInstance.reset_mock() 52 | 53 | menu.message("1\n1\n1") 54 | 55 | assert LCDHwdMockInstance.write4bits.mock_calls == [ 56 | call(ord("1"), True), call(0xC0), call(ord("1"), True), call(0xC0) 57 | ] 58 | 59 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 60 | def test_rpilcdmenu_displayTestScreen_sends_dummy_message_to_rpi(LCDHwdMock): 61 | LCDHwdMockInstance = MagicMock() 62 | LCDHwdMock.return_value = LCDHwdMockInstance 63 | 64 | menu = RpiLCDMenu() 65 | LCDHwdMockInstance.reset_mock() 66 | 67 | menu.displayTestScreen() 68 | 69 | LCDHwdMockInstance.write4bits.assert_called() 70 | 71 | 72 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 73 | def test_rpilcdmenu_render_empty_menu(LCDHwdMock): 74 | LCDHwdMockInstance = MagicMock() 75 | LCDHwdMock.return_value = LCDHwdMockInstance 76 | 77 | menu = RpiLCDMenu() 78 | menu.start() 79 | LCDHwdMockInstance.reset_mock() 80 | 81 | menu.render() 82 | 83 | assert LCDHwdMockInstance.write4bits.mock_calls == [call(LCDHwdMock.LCD_CLEARDISPLAY)] + [ 84 | call(ord(char), True) for char in "Menu is empty" 85 | ] 86 | 87 | 88 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 89 | def test_rpilcdmenu_render_two_items_menu(LCDHwdMock): 90 | LCDHwdMockInstance = MagicMock() 91 | LCDHwdMock.return_value = LCDHwdMockInstance 92 | 93 | menu = RpiLCDMenu() 94 | LCDHwdMockInstance.reset_mock() 95 | 96 | item1Mock = Mock() 97 | item1Mock.text = "item1" 98 | item2Mock = Mock() 99 | item2Mock.text = "item2" 100 | 101 | menu.append_item(item1Mock) 102 | menu.append_item(item2Mock) 103 | 104 | menu.render() 105 | 106 | assert LCDHwdMockInstance.write4bits.mock_calls == [call(LCDHwdMock.LCD_CLEARDISPLAY)] + [ 107 | call(ord(char), True) for char in ">item1" 108 | ] + [call(0xC0)] + [ 109 | call(ord(char), True) for char in " item2" 110 | ] 111 | 112 | 113 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 114 | def test_rpilcdmenu_render_multiple_items_menu(LCDHwdMock): 115 | LCDHwdMockInstance = MagicMock() 116 | LCDHwdMock.return_value = LCDHwdMockInstance 117 | 118 | menu = RpiLCDMenu() 119 | 120 | item1Mock = Mock() 121 | item1Mock.text = "item1" 122 | item2Mock = Mock() 123 | item2Mock.text = "item2" 124 | item3Mock = Mock() 125 | item3Mock.text = "item3" 126 | 127 | menu.append_item(item1Mock) 128 | menu.append_item(item2Mock) 129 | menu.append_item(item3Mock) 130 | 131 | menu.processDown() 132 | LCDHwdMockInstance.reset_mock() 133 | menu.render() 134 | 135 | assert LCDHwdMockInstance.write4bits.mock_calls == [call(LCDHwdMock.LCD_CLEARDISPLAY)] + [ 136 | call(ord(char), True) for char in ">item2" 137 | ] + [call(0xC0)] + [ 138 | call(ord(char), True) for char in " item3" 139 | ] 140 | 141 | 142 | @patch('rpilcdmenu.rpi_lcd_menu.RpiLCDHwd') 143 | def test_rpilcdmenu_render_multiple_items_rewind_menu(LCDHwdMock): 144 | LCDHwdMockInstance = MagicMock() 145 | LCDHwdMock.return_value = LCDHwdMockInstance 146 | 147 | menu = RpiLCDMenu() 148 | 149 | item1Mock = Mock() 150 | item1Mock.text = "item1" 151 | item2Mock = Mock() 152 | item2Mock.text = "item2" 153 | item3Mock = Mock() 154 | item3Mock.text = "item3" 155 | 156 | menu.append_item(item1Mock) 157 | menu.append_item(item2Mock) 158 | menu.append_item(item3Mock) 159 | 160 | menu.processDown() 161 | menu.processDown() 162 | LCDHwdMockInstance.reset_mock() 163 | menu.render() 164 | 165 | assert LCDHwdMockInstance.write4bits.mock_calls == [call(LCDHwdMock.LCD_CLEARDISPLAY)] + [ 166 | call(ord(char), True) for char in ">item3" 167 | ] + [call(0xC0)] + [ 168 | call(ord(char), True) for char in " item1" 169 | ] 170 | 171 | --------------------------------------------------------------------------------