├── docs ├── readme_link.rst ├── changelog_link.rst ├── CONTRIBUTING.md ├── lcd_i2c.rst ├── index.rst ├── requirements.txt ├── DOCUMENTATION.md ├── conf.py └── EXAMPLES.md ├── requirements.txt ├── lcd_i2c ├── __init__.py ├── version.py ├── const.py ├── typing.py └── lcd_i2c.py ├── requirements-deploy.txt ├── codecov.yaml ├── requirements-test.txt ├── .yamllint ├── examples ├── boot.py └── main.py ├── create_report_dirs.py ├── .editorconfig ├── .readthedocs.yaml ├── .coveragerc ├── tests ├── unittest.cfg ├── test_absolute_truth.py └── test_lcd_i2c.py ├── package.json ├── LICENSE.txt ├── .github └── workflows │ ├── unittest.yaml │ ├── test.yml │ ├── release.yml │ └── test-release.yaml ├── changelog.md ├── setup.py ├── .flake8 ├── .gitignore ├── sdist_upip.py └── README.md /docs/readme_link.rst: -------------------------------------------------------------------------------- 1 | 2 | .. include:: ../README.md 3 | :parser: myst_parser.sphinx_ -------------------------------------------------------------------------------- /docs/changelog_link.rst: -------------------------------------------------------------------------------- 1 | 2 | .. include:: ../changelog.md 3 | :parser: myst_parser.sphinx_ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # List external packages here 2 | # Avoid fixed versions 3 | rshell>=0.0.30,<1.0.0 4 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Guideline to contribute to this package 4 | 5 | --------------- 6 | 7 | ## TBD 8 | -------------------------------------------------------------------------------- /lcd_i2c/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .version import __version__ 5 | 6 | from .lcd_i2c import LCD 7 | -------------------------------------------------------------------------------- /lcd_i2c/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | __version_info__ = ("0", "0", "0") 5 | __version__ = '.'.join(__version_info__) 6 | -------------------------------------------------------------------------------- /requirements-deploy.txt: -------------------------------------------------------------------------------- 1 | # List external packages here 2 | # Avoid fixed versions 3 | # # to upload package to PyPi or other package hosts 4 | twine>=4.0.1,<5 5 | changelog2version>=0.5.0,<1 -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | codecov: 4 | bot: brainelectronics 5 | 6 | coverage: 7 | status: 8 | project: 9 | default: 10 | target: 100% 11 | threshold: 1% 12 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # List external packages here 2 | # Avoid fixed versions 3 | flake8>=5.0.0,<6 4 | coverage>=6.4.2,<7 5 | nose2>=0.12.0,<1 6 | yamllint>=1.29,<2 7 | setup2upypackage>=0.2.0,<1 8 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | ignore: 6 | - .tox 7 | - .venv 8 | 9 | rules: 10 | line-length: 11 | level: warning 12 | ignore: 13 | - .github/workflows/* 14 | -------------------------------------------------------------------------------- /examples/boot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Boot script 6 | 7 | Do initial stuff here, similar to the setup() function on Arduino 8 | """ 9 | 10 | # This file is executed on every boot (including wake-boot from deepsleep) 11 | # import esp 12 | # esp.osdebug(None) 13 | # import webrepl 14 | # webrepl.start() 15 | 16 | print('System booted successfully!') 17 | -------------------------------------------------------------------------------- /create_report_dirs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Create test report directories.""" 3 | from pathlib import Path 4 | import os 5 | import shutil 6 | 7 | if os.path.exists('reports'): 8 | shutil.rmtree('reports', ignore_errors=True) 9 | 10 | Path('reports/sca').mkdir(parents=True, exist_ok=True) 11 | Path('reports/test_results').mkdir(parents=True, exist_ok=True) 12 | Path('reports/coverage').mkdir(parents=True, exist_ok=True) 13 | -------------------------------------------------------------------------------- /docs/lcd_i2c.rst: -------------------------------------------------------------------------------- 1 | API 2 | ======================= 3 | 4 | .. autosummary:: 5 | :toctree: generated 6 | 7 | LCD 8 | --------------------------------- 9 | 10 | .. automodule:: lcd_i2c.lcd_i2c 11 | :members: 12 | :private-members: 13 | :show-inheritance: 14 | 15 | HD44780 Constants 16 | --------------------------------- 17 | 18 | .. automodule:: lcd_i2c.const 19 | :members: 20 | :private-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | MicroPython package to control HD44780 LCD displays via I2C 2 | =========================================================== 3 | 4 | Contents 5 | -------- 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | readme_link 11 | EXAMPLES 12 | DOCUMENTATION 13 | CONTRIBUTING 14 | lcd_i2c 15 | changelog_link 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # use fixed versions 2 | # 3 | # fix docutils to a version working for all 4 | docutils >=0.14,<0.18 5 | 6 | # sphinx 5.3.0 requires Jinja2 >=3.0 and docutils >=0.14,<0.20 7 | sphinx >=5.0.0,<6 8 | 9 | # sphinx-rtd-theme >=1.0.0 would require docutils <0.18 10 | sphinx-rtd-theme >=1.0.0,<2 11 | 12 | # replaces outdated and no longer maintained m2rr 13 | myst-parser >= 0.18.1,<1 14 | 15 | # mock imports of "micropython" 16 | mock >=4.0.3,<5 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | # 4 space indentation 15 | [*.py] 16 | indent_size = 4 17 | 18 | [*.json] 19 | indent_size = 4 20 | 21 | # 2 space indentation 22 | [*.yml] 23 | indent_size = 2 24 | 25 | [*.{md,rst}] 26 | indent_size = 4 27 | trim_trailing_whitespace = false 28 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Read the Docs configuration file 4 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 5 | 6 | # Required 7 | version: 2 8 | 9 | # Set the version of Python and other tools you might need 10 | build: 11 | os: ubuntu-22.04 12 | tools: 13 | python: "3.9" 14 | 15 | # Build documentation in the docs/ directory with Sphinx 16 | sphinx: 17 | configuration: docs/conf.py 18 | 19 | # Optionally declare the Python requirements required to build your docs 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # Configuration for python coverage package 2 | [run] 3 | branch = True 4 | omit = 5 | */tests/*, 6 | .venv/*, 7 | .idea/*, 8 | setup*, 9 | .eggs/* 10 | .tox/*, 11 | build/*, 12 | dist/*, 13 | version.py, 14 | lcd_i2c/const.py, 15 | lcd_i2c/typing.py, 16 | 17 | [report] 18 | # include = src/* 19 | include = lcd_i2c/* 20 | # Regexes for lines to exclude from consideration 21 | 22 | ignore_errors = True 23 | 24 | [html] 25 | directory = reports/coverage/html 26 | skip_empty = True 27 | 28 | [xml] 29 | output = reports/coverage/coverage.xml 30 | 31 | [json] 32 | output = reports/coverage/coverage.json 33 | pretty_print = True 34 | show_contexts = True 35 | -------------------------------------------------------------------------------- /tests/unittest.cfg: -------------------------------------------------------------------------------- 1 | [unittest] 2 | # start-dir = tests 3 | # code-directories = src 4 | plugins = nose2.plugins.junitxml 5 | # plugins = nose2.plugins.attrib 6 | 7 | [coverage] 8 | always-on = True 9 | coverage = 10 | coverage-config = 11 | coverage-report = html 12 | 13 | [discovery] 14 | always-on = True 15 | 16 | [functions] 17 | always-on = True 18 | 19 | [output-buffer] 20 | # set "always-on" to False to see print content in console 21 | always-on = True 22 | stderr = False 23 | stdout = True 24 | 25 | [parameters] 26 | always-on = True 27 | 28 | [pretty-assert] 29 | always-on = True 30 | 31 | [test-result] 32 | always-on = True 33 | descriptions = True 34 | 35 | [junit-xml] 36 | always-on = True 37 | keep_restricted = False 38 | path = reports/test_results/nose2-junit.xml 39 | test_fullname = True 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | [ 4 | "lcd_i2c/__init__.py", 5 | "github:brainelectronics/micropython-i2c-lcd/lcd_i2c/__init__.py" 6 | ], 7 | [ 8 | "lcd_i2c/const.py", 9 | "github:brainelectronics/micropython-i2c-lcd/lcd_i2c/const.py" 10 | ], 11 | [ 12 | "lcd_i2c/lcd_i2c.py", 13 | "github:brainelectronics/micropython-i2c-lcd/lcd_i2c/lcd_i2c.py" 14 | ], 15 | [ 16 | "lcd_i2c/typing.py", 17 | "github:brainelectronics/micropython-i2c-lcd/lcd_i2c/typing.py" 18 | ], 19 | [ 20 | "lcd_i2c/version.py", 21 | "github:brainelectronics/micropython-i2c-lcd/lcd_i2c/version.py" 22 | ] 23 | ], 24 | "deps": [], 25 | "version": "0.1.0" 26 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 brainelectronics and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # this file is *not* meant to cover or endorse the use of GitHub Actions, but 4 | # rather to help run automated tests for this project 5 | 6 | name: Unittest Python Package 7 | 8 | on: [push, pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test-and-coverage: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-python@v3 19 | with: 20 | python-version: '3.9' 21 | - name: Execute tests 22 | run: | 23 | pip install -r requirements-test.txt 24 | python create_report_dirs.py 25 | nose2 --config tests/unittest.cfg 26 | - name: Create coverage report 27 | run: | 28 | coverage xml 29 | - name: Upload coverage to Codecov 30 | uses: codecov/codecov-action@v3 31 | with: 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | files: ./reports/coverage/coverage.xml 34 | flags: unittests 35 | fail_ci_if_error: true 36 | # path_to_write_report: ./reports/coverage/codecov_report.txt 37 | verbose: true 38 | -------------------------------------------------------------------------------- /docs/DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | Documentation is generated by using Sphinx and published on RTD 4 | 5 | --------------- 6 | 7 | ## Documentation 8 | 9 | Documentation is automatically created on each merge to the development 10 | branch, as well as with each pull request and available 11 | [📚 here at Read the Docs][ref-rtd-micropython-i2c-lcd] 12 | 13 | ### Install required packages 14 | 15 | ```bash 16 | # create and activate virtual environment 17 | python3 -m venv .venv 18 | source .venv/bin/activate 19 | 20 | # install and upgrade required packages 21 | pip install -U -r docs/requirements.txt 22 | ``` 23 | 24 | ### Create documentation 25 | 26 | Some usefull checks have been disabled in the `docs/conf.py` file. Please 27 | check the documentation build output locally before opening a PR. 28 | 29 | ```bash 30 | # perform link checks 31 | sphinx-build docs/ docs/build/linkcheck -d docs/build/docs_doctree/ --color -blinkcheck -j auto -W 32 | 33 | # create documentation 34 | sphinx-build docs/ docs/build/html/ -d docs/build/docs_doctree/ --color -bhtml -j auto -W 35 | ``` 36 | 37 | The created documentation can be found at `docs/build/html`. 38 | 39 | 40 | [ref-rtd-micropython-i2c-lcd]: https://micropython-i2c-lcd.readthedocs.io/en/latest/ 41 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 14 | 18 | 19 | ## Released 20 | ## [0.1.1] - 2023-06-12 21 | ### Fixed 22 | - Usage documentation with more comments and WiFi instructions in root README 23 | - Validate `package.json` file in test workflow 24 | 25 | ## [0.1.0] - 2023-03-09 26 | ### Added 27 | - `const.py`, `display.py` and `typing.py` in `lcd_i2c` module 28 | - Examples and documentation 29 | 30 | ### Changed 31 | - Several updates on setup and config files different than the template repo 32 | 33 | ### Removed 34 | - Not used files provided with [template repo](https://github.com/brainelectronics/micropython-i2c-lcd) 35 | 36 | 37 | [Unreleased]: https://github.com/brainelectronics/micropython-i2c-lcd/compare/0.1.1...main 38 | 39 | [0.1.1]: https://github.com/brainelectronics/micropython-i2c-lcd/tree/0.1.1 40 | [0.1.0]: https://github.com/brainelectronics/micropython-i2c-lcd/tree/0.1.0 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from setuptools import setup 5 | from pathlib import Path 6 | import sdist_upip 7 | 8 | here = Path(__file__).parent.resolve() 9 | 10 | # Get the long description from the README file 11 | long_description = (here / 'README.md').read_text(encoding='utf-8') 12 | 13 | # load elements of version.py 14 | exec(open(here / 'lcd_i2c' / 'version.py').read()) 15 | 16 | setup( 17 | name='micropython-i2c-lcd', 18 | version=__version__, 19 | description="MicroPython package to control HD44780 LCD displays 1602 and 2004", 20 | long_description=long_description, 21 | long_description_content_type='text/markdown', 22 | url='https://github.com/brainelectronics/micropython-i2c-lcd', 23 | author='brainelectronics', 24 | author_email='info@brainelectronics.de', 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Programming Language :: Python :: Implementation :: MicroPython', 30 | ], 31 | keywords='micropython, HD44780, I2C, display, LCD1602, LCD2004, PCF8574', 32 | project_urls={ 33 | 'Bug Reports': 'https://github.com/brainelectronics/micropython-i2c-lcd/issues', 34 | 'Source': 'https://github.com/brainelectronics/micropython-i2c-lcd', 35 | }, 36 | license='MIT', 37 | cmdclass={'sdist': sdist_upip.sdist}, 38 | packages=['lcd_i2c'], 39 | install_requires=[] 40 | ) 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # This workflow will install Python dependencies, run tests and lint with a 4 | # specific Python version 5 | # For more information see: 6 | # https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 7 | 8 | name: Test Python package 9 | 10 | on: 11 | push: 12 | # branches: [ $default-branch ] 13 | branches-ignore: 14 | - 'main' 15 | - 'develop' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | - name: Set up Python 27 | uses: actions/setup-python@v3 28 | with: 29 | python-version: '3.9' 30 | - name: Install test dependencies 31 | run: | 32 | pip install -r requirements-test.txt 33 | - name: Lint with flake8 34 | run: | 35 | flake8 . 36 | - name: Lint with yamllint 37 | run: | 38 | yamllint . 39 | - name: Install deploy dependencies 40 | run: | 41 | python -m pip install --upgrade pip 42 | if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi 43 | - name: Build package 44 | run: | 45 | changelog2version \ 46 | --changelog_file changelog.md \ 47 | --version_file lcd_i2c/version.py \ 48 | --version_file_type py \ 49 | --debug 50 | python setup.py sdist 51 | rm dist/*.orig 52 | - name: Test built package 53 | run: | 54 | twine check dist/* 55 | - name: Validate mip package file 56 | run: | 57 | upy-package \ 58 | --setup_file setup.py \ 59 | --package_changelog_file changelog.md \ 60 | --package_file package.json \ 61 | --validate \ 62 | --ignore-version 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # this file is *not* meant to cover or endorse the use of GitHub Actions, but 4 | # rather to help make automated releases for this project 5 | 6 | name: Upload Python Package 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | deploy: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | - name: Set up Python 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: '3.9' 26 | - name: Install build dependencies 27 | run: | 28 | if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi 29 | - name: Build package 30 | run: | 31 | changelog2version \ 32 | --changelog_file changelog.md \ 33 | --version_file lcd_i2c/version.py \ 34 | --version_file_type py \ 35 | --debug 36 | python setup.py sdist 37 | rm dist/*.orig 38 | # sdist call create non conform twine files *.orig, remove them 39 | - name: Publish package 40 | uses: pypa/gh-action-pypi-publish@release/v1.5 41 | with: 42 | password: ${{ secrets.PYPI_API_TOKEN }} 43 | skip_existing: true 44 | verbose: true 45 | print_hash: true 46 | - name: 'Create changelog based release' 47 | uses: brainelectronics/changelog-based-release@v1 48 | with: 49 | # note you'll typically need to create a personal access token 50 | # with permissions to create releases in the other repo 51 | # or you set the "contents" permissions to "write" as in this example 52 | changelog-path: changelog.md 53 | tag-name-prefix: '' 54 | tag-name-extension: '' 55 | release-name-prefix: '' 56 | release-name-extension: '' 57 | draft-release: true 58 | prerelease: false 59 | -------------------------------------------------------------------------------- /lcd_i2c/const.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | try: 5 | from micropython import const 6 | except ImportError: 7 | def const(x): 8 | return x 9 | 10 | # commands 11 | #: Clear display command 12 | LCD_CLEARDISPLAY = const(0x01) 13 | #: Return to home position command 14 | LCD_RETURNHOME = const(0x02) 15 | #: Set entry mode command 16 | LCD_ENTRYMODESET = const(0x04) 17 | #: Control display command 18 | LCD_DISPLAYCONTROL = const(0x08) 19 | #: Shift cursor command 20 | LCD_CURSORSHIFT = const(0x10) 21 | #: Set function command 22 | LCD_FUNCTIONSET = const(0x20) 23 | #: Set CGRAM address command 24 | LCD_SETCGRAMADDR = const(0x40) 25 | #: Set DDRAM address command 26 | LCD_SETDDRAMADDR = const(0x80) 27 | 28 | # flags for display entry mode 29 | #: Set display entry mode as right command 30 | LCD_ENTRYRIGHT = const(0x00) 31 | #: Set display entry mode as left command 32 | LCD_ENTRYLEFT = const(0x02) 33 | #: Set display entry mode as shift increment command 34 | LCD_ENTRYSHIFTINCREMENT = const(0x01) 35 | #: Set display entry mode as shift decrement command 36 | LCD_ENTRYSHIFTDECREMENT = const(0x00) 37 | 38 | # flags for display on/off control 39 | #: Turn display on command 40 | LCD_DISPLAYON = const(0x04) 41 | #: Turn display off command 42 | LCD_DISPLAYOFF = const(0x00) 43 | #: Turn cursor on command 44 | LCD_CURSORON = const(0x02) 45 | #: Turn cursor off command 46 | LCD_CURSOROFF = const(0x00) 47 | #: Set curor blink command 48 | LCD_BLINKON = const(0x01) 49 | #: Set curor no blink command 50 | LCD_BLINKOFF = const(0x00) 51 | 52 | # flags for display/cursor shift 53 | #: Display move command 54 | LCD_DISPLAYMOVE = const(0x08) 55 | #: Move cursor command 56 | LCD_CURSORMOVE = const(0x00) 57 | #: Move display shift right command 58 | LCD_MOVERIGHT = const(0x04) 59 | #: Move display shift left command 60 | LCD_MOVELEFT = const(0x00) 61 | 62 | # flags for function set 63 | #: 8 bit mode command 64 | LCD_8BITMODE = const(0x10) 65 | #: 4 bit mode command 66 | LCD_4BITMODE = const(0x00) 67 | #: 2 line command 68 | LCD_2LINE = const(0x08) 69 | #: 1 line command 70 | LCD_1LINE = const(0x00) 71 | #: 5x10 dots display command 72 | LCD_5x10DOTS = const(0x04) 73 | #: 5x8 dots display command 74 | LCD_5x8DOTS = const(0x00) 75 | 76 | # flags for backlight control 77 | #: Activate backlight command 78 | LCD_BACKLIGHT = const(0x08) 79 | #: Deactivate backlight command 80 | LCD_NOBACKLIGHT = const(0x00) 81 | 82 | # other 83 | #: Enable bit 84 | EN = const(0b00000100) 85 | #: Read/Write bit 86 | RW = const(0b00000010) 87 | #: Register select bit 88 | RS = const(0b00000001) 89 | -------------------------------------------------------------------------------- /.github/workflows/test-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # this file is *not* meant to cover or endorse the use of GitHub Actions, but 4 | # rather to help make automated test releases for this project 5 | 6 | name: Upload Python Package to test.pypi.org 7 | 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | test-deploy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Set up Python 20 | uses: actions/setup-python@v3 21 | with: 22 | python-version: '3.9' 23 | - name: Install build dependencies 24 | run: | 25 | if [ -f requirements-deploy.txt ]; then pip install -r requirements-deploy.txt; fi 26 | - name: Build package 27 | run: | 28 | changelog2version \ 29 | --changelog_file changelog.md \ 30 | --version_file lcd_i2c/version.py \ 31 | --version_file_type py \ 32 | --additional_version_info="-rc${{ github.run_number }}.dev${{ github.event.number }}" \ 33 | --debug 34 | python setup.py sdist 35 | - name: Test built package 36 | # sdist call creates non twine conform "*.orig" files, remove them 37 | run: | 38 | rm dist/*.orig 39 | twine check dist/*.tar.gz 40 | - name: Archive build package artifact 41 | uses: actions/upload-artifact@v3 42 | with: 43 | # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context 44 | # ${{ github.repository }} and ${{ github.ref_name }} can't be used 45 | # for artifact name due to unallowed '/' 46 | name: dist_repo.${{ github.event.repository.name }}_sha.${{ github.sha }}_build.${{ github.run_number }} 47 | path: dist/*.tar.gz 48 | retention-days: 14 49 | - name: Publish package 50 | uses: pypa/gh-action-pypi-publish@release/v1.5 51 | with: 52 | repository_url: https://test.pypi.org/legacy/ 53 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 54 | skip_existing: true 55 | verbose: true 56 | print_hash: true 57 | - name: 'Create changelog based prerelease' 58 | uses: brainelectronics/changelog-based-release@v1 59 | with: 60 | # note you'll typically need to create a personal access token 61 | # with permissions to create releases in the other repo 62 | # or you set the "contents" permissions to "write" as in this example 63 | changelog-path: changelog.md 64 | tag-name-prefix: '' 65 | tag-name-extension: '-rc${{ github.run_number }}.dev${{ github.event.number }}' 66 | release-name-prefix: '' 67 | release-name-extension: '-rc${{ github.run_number }}.dev${{ github.event.number }}' 68 | draft-release: true 69 | prerelease: true 70 | -------------------------------------------------------------------------------- /tests/test_absolute_truth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | """Unittest for testing the absolute truth""" 4 | 5 | import logging 6 | from nose2.tools import params 7 | import sys 8 | from typing import Any 9 | import unittest 10 | 11 | 12 | class TestAbsoluteTruth(unittest.TestCase): 13 | def setUp(self) -> None: 14 | """Run before every test method""" 15 | # define a format 16 | custom_format = "[%(asctime)s][%(levelname)-8s][%(filename)-20s @" \ 17 | " %(funcName)-15s:%(lineno)4s] %(message)s" 18 | 19 | # set basic config and level for all loggers 20 | logging.basicConfig(level=logging.INFO, 21 | format=custom_format, 22 | stream=sys.stdout) 23 | 24 | # create a logger for this TestSuite 25 | self.test_logger = logging.getLogger(__name__) 26 | 27 | # set the test logger level 28 | self.test_logger.setLevel(logging.DEBUG) 29 | 30 | # enable/disable the log output of the device logger for the tests 31 | # if enabled log data inside this test will be printed 32 | self.test_logger.disabled = False 33 | 34 | def test_absolute_truth(self) -> None: 35 | """Test the unittest itself""" 36 | x = 0 37 | y = 1 38 | z = 2 39 | none_thing = None 40 | some_dict = dict() 41 | some_list = [x, y, 40, "asdf", z] 42 | 43 | self.assertTrue(True) 44 | self.assertFalse(False) 45 | 46 | self.assertEqual(y, 1) 47 | assert y == 1 48 | self.assertNotEqual(x, y) 49 | assert x != y 50 | 51 | self.assertIsNone(none_thing) 52 | self.assertIsNotNone(some_dict) 53 | 54 | self.assertIn(y, some_list) 55 | self.assertNotIn(12, some_list) 56 | 57 | # self.assertRaises(exc, fun, args, *kwds) 58 | 59 | self.assertIsInstance(some_dict, dict) 60 | self.assertNotIsInstance(some_dict, list) 61 | 62 | self.assertGreater(a=y, b=x) 63 | self.assertGreaterEqual(a=y, b=x) 64 | self.assertLess(a=x, b=y) 65 | 66 | self.test_logger.debug("Sample debug message") 67 | 68 | @params( 69 | (123.45, True), 70 | (1, False) 71 | ) 72 | def test_with_params(self, parameter: Any, expectation: bool) -> None: 73 | """ 74 | Test something using parameters 75 | 76 | :param parameter: The parameter value 77 | :type parameter: Any 78 | :param expectation: The expectation 79 | :type expectation: bool 80 | """ 81 | if expectation: 82 | self.assertIsInstance(parameter, float) 83 | else: 84 | self.assertNotIsInstance(parameter, float) 85 | 86 | def tearDown(self) -> None: 87 | """Run after every test method""" 88 | pass 89 | 90 | 91 | if __name__ == '__main__': 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /lcd_i2c/typing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | Fake classes of typing module. 6 | 7 | https://github.com/micropython/micropython-lib/issues/190 8 | 9 | https://github.com/micropython/micropython-lib/blob/3d779b8ceab5b65b9f70accbcbb15ab3509eceb7/typing/typing.py 10 | """ 11 | 12 | 13 | class _Subscriptable(): 14 | def __getitem__(self, item): 15 | return None 16 | 17 | 18 | _subscriptable = _Subscriptable() 19 | 20 | 21 | class Any: 22 | pass 23 | 24 | 25 | class NoReturn: 26 | pass 27 | 28 | 29 | class ClassVar: 30 | pass 31 | 32 | 33 | Union = _subscriptable 34 | 35 | 36 | Optional = _subscriptable 37 | 38 | 39 | class Generic: 40 | pass 41 | 42 | 43 | class NamedTuple: 44 | pass 45 | 46 | 47 | class Hashable: 48 | pass 49 | 50 | 51 | class Awaitable: 52 | pass 53 | 54 | 55 | class Coroutine: 56 | pass 57 | 58 | 59 | class AsyncIterable: 60 | pass 61 | 62 | 63 | class AsyncIterator: 64 | pass 65 | 66 | 67 | class Iterable: 68 | pass 69 | 70 | 71 | class Iterator: 72 | pass 73 | 74 | 75 | class Reversible: 76 | pass 77 | 78 | 79 | class Sized: 80 | pass 81 | 82 | 83 | class Container: 84 | pass 85 | 86 | 87 | class Collection: 88 | pass 89 | 90 | 91 | # class Callable: 92 | # pass 93 | Callable = _subscriptable 94 | 95 | 96 | class AbstractSet: 97 | pass 98 | 99 | 100 | class MutableSet: 101 | pass 102 | 103 | 104 | class Mapping: 105 | pass 106 | 107 | 108 | class MutableMapping: 109 | pass 110 | 111 | 112 | class Sequence: 113 | pass 114 | 115 | 116 | class MutableSequence: 117 | pass 118 | 119 | 120 | class ByteString: 121 | pass 122 | 123 | 124 | Tuple = _subscriptable 125 | 126 | 127 | List = _subscriptable 128 | 129 | 130 | class Deque: 131 | pass 132 | 133 | 134 | class Set: 135 | pass 136 | 137 | 138 | class dict_keys: 139 | pass 140 | 141 | 142 | class FrozenSet: 143 | pass 144 | 145 | 146 | class MappingView: 147 | pass 148 | 149 | 150 | class KeysView: 151 | pass 152 | 153 | 154 | class ItemsView: 155 | pass 156 | 157 | 158 | class ValuesView: 159 | pass 160 | 161 | 162 | class ContextManager: 163 | pass 164 | 165 | 166 | class AsyncContextManager: 167 | pass 168 | 169 | 170 | Dict = _subscriptable 171 | 172 | 173 | class DefaultDict: 174 | pass 175 | 176 | 177 | class Counter: 178 | pass 179 | 180 | 181 | class ChainMap: 182 | pass 183 | 184 | 185 | class Generator: 186 | pass 187 | 188 | 189 | class AsyncGenerator: 190 | pass 191 | 192 | 193 | class Type: 194 | pass 195 | 196 | 197 | def cast(typ, val): 198 | return val 199 | 200 | 201 | def _overload_dummy(*args, **kwds): 202 | """Helper for @overload to raise when called.""" 203 | raise NotImplementedError( 204 | "You should not call an overloaded function. " 205 | "A series of @overload-decorated functions " 206 | "outside a stub module should always be followed " 207 | "by an implementation that is not @overload-ed." 208 | ) 209 | 210 | 211 | def overload(): 212 | return _overload_dummy 213 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | sys.path.insert(0, os.path.abspath('../')) 14 | here = Path(__file__).parent.resolve() 15 | 16 | try: 17 | # Inject mock modules so that we can build the 18 | # documentation without having the real stuff available 19 | from mock import Mock 20 | 21 | to_be_mocked = [ 22 | 'micropython', 23 | 'machine', 24 | 'time.sleep_ms', 'time.sleep_us', 25 | ] 26 | for module in to_be_mocked: 27 | sys.modules[module] = Mock() 28 | print("Mocked '{}' module".format(module)) 29 | 30 | from lcd_i2c import LCD 31 | except ImportError: 32 | raise SystemExit("lcd_i2c has to be importable") 33 | else: 34 | pass 35 | 36 | # load elements of version.py 37 | exec(open(here / '..' / 'lcd_i2c' / 'version.py').read()) 38 | 39 | # -- Project information 40 | 41 | project = 'micropython-i2c-lcd' 42 | copyright = '2023, brainelectronics' 43 | author = 'brainelectronics' 44 | 45 | version = __version__ 46 | release = version 47 | 48 | # -- General configuration 49 | 50 | # Add any Sphinx extension module names here, as strings. They can be 51 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 52 | # ones. 53 | extensions = [ 54 | 'myst_parser', 55 | 'sphinx.ext.autodoc', 56 | 'sphinx.ext.autosectionlabel', 57 | 'sphinx.ext.autosummary', 58 | 'sphinx.ext.doctest', 59 | 'sphinx.ext.duration', 60 | 'sphinx.ext.intersphinx', 61 | 'sphinx.ext.viewcode', 62 | ] 63 | autosectionlabel_prefix_document = True 64 | 65 | # The suffix of source filenames. 66 | source_suffix = ['.rst', '.md'] 67 | 68 | intersphinx_mapping = { 69 | 'python': ('https://docs.python.org/3/', None), 70 | 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), 71 | } 72 | intersphinx_disabled_domains = ['std'] 73 | suppress_warnings = [ 74 | # throws an error due to not found reference targets to files not in docs/ 75 | 'ref.myst', 76 | # throws an error due to multiple "Added" labels in "changelog.md" 77 | 'autosectionlabel.*' 78 | ] 79 | 80 | # A list of regular expressions that match URIs that should not be checked 81 | # when doing a linkcheck build. 82 | linkcheck_ignore = [ 83 | # tag 0.1.0 did not exist during docs introduction 84 | 'https://github.com/brainelectronics/micropython-i2c-lcd/tree/0.1.0', 85 | # RTD page did not exist during docs introduction 86 | 'https://micropython-i2c-lcd.readthedocs.io/en/latest/', 87 | # examples folder did not exist during docs introduction 88 | 'https://github.com/brainelectronics/micropython-i2c-lcd/tree/develop/examples', 89 | ] 90 | 91 | templates_path = ['_templates'] 92 | 93 | # -- Options for HTML output 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | # 98 | html_theme = 'sphinx_rtd_theme' 99 | 100 | # -- Options for EPUB output 101 | epub_show_urls = 'footnote' 102 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # Configuration for flake8 analysis 2 | [flake8] 3 | # Set the maximum length that any line (with some exceptions) may be. 4 | # max-line-length = 120 5 | 6 | # Set the maximum length that a comment or docstring line may be. 7 | # max-doc-length = 120 8 | 9 | # Set the maximum allowed McCabe complexity value for a block of code. 10 | # max-complexity = 15 11 | 12 | # Specify a list of codes to ignore. 13 | # D107: Missing docstring in __init__ 14 | # D400: First line should end with a period 15 | # W504: line break after binary operator -> Cannot break line with a long pathlib Path 16 | # D204: 1 blank line required after class docstring 17 | ignore = D107, D400, W504, D204 18 | 19 | # Specify a list of mappings of files and the codes that should be ignored for the entirety of the file. 20 | per-file-ignores = 21 | tests/*:D101,D102,D104 22 | 23 | # Provide a comma-separated list of glob patterns to exclude from checks. 24 | exclude = 25 | # No need to traverse our git directory 26 | .git, 27 | # Python virtual environments 28 | .venv, 29 | # tox virtual environments 30 | .tox, 31 | # There's no value in checking cache directories 32 | __pycache__, 33 | # The conf file is mostly autogenerated, ignore it 34 | docs/conf.py, 35 | # This contains our built documentation 36 | build, 37 | # This contains builds that we don't want to check 38 | dist, 39 | # We don't use __init__.py for scripts 40 | __init__.py 41 | # example testing folder before going live 42 | thinking 43 | .idea 44 | # custom scripts, not being part of the distribution 45 | libs_external 46 | sdist_upip.py 47 | setup.py 48 | 49 | # Provide a comma-separated list of glob patterns to add to the list of excluded ones. 50 | # extend-exclude = 51 | # legacy/, 52 | # vendor/ 53 | 54 | # Provide a comma-separate list of glob patterns to include for checks. 55 | # filename = 56 | # example.py, 57 | # another-example*.py 58 | 59 | # Enable PyFlakes syntax checking of doctests in docstrings. 60 | doctests = False 61 | 62 | # Specify which files are checked by PyFlakes for doctest syntax. 63 | # include-in-doctest = 64 | # dir/subdir/file.py, 65 | # dir/other/file.py 66 | 67 | # Specify which files are not to be checked by PyFlakes for doctest syntax. 68 | # exclude-from-doctest = 69 | # tests/* 70 | 71 | # Enable off-by-default extensions. 72 | # enable-extensions = 73 | # H111, 74 | # G123 75 | 76 | # If True, report all errors, even if it is on the same line as a # NOQA comment. 77 | disable-noqa = False 78 | 79 | # Specify the number of subprocesses that Flake8 will use to run checks in parallel. 80 | jobs = auto 81 | 82 | # Also print output to stdout if output-file has been configured. 83 | tee = True 84 | 85 | # Count the number of occurrences of each error/warning code and print a report. 86 | statistics = True 87 | 88 | # Print the total number of errors. 89 | count = True 90 | 91 | # Print the source code generating the error/warning in question. 92 | show-source = True 93 | 94 | # Decrease the verbosity of Flake8’s output. Each time you specify it, it will print less and less information. 95 | quiet = 0 96 | 97 | # Select the formatter used to display errors to the user. 98 | format = pylint 99 | 100 | [pydocstyle] 101 | # choose the basic list of checked errors by specifying an existing convention. Possible conventions: pep257, numpy, google. 102 | convention = pep257 103 | 104 | # check only files that exactly match regular expression 105 | # match = (?!test_).*\.py 106 | 107 | # search only dirs that exactly match regular expression 108 | # match_dir = [^\.].* 109 | 110 | # ignore any functions or methods that are decorated by a function with a name fitting the regular expression. 111 | # ignore_decorators = 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # custom, package specific ignores 2 | .DS_Store 3 | .DS_Store? 4 | pymakr.conf 5 | config/config*.py 6 | thinking/ 7 | *.bin 8 | .idea 9 | *.bak 10 | *.o 11 | .vagrant/ 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | cover/ 65 | reports/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | .pybuilder/ 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | # For a library or package, you might want to ignore these files since the code is 100 | # intended to run in multiple environments; otherwise, check them in: 101 | # .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # poetry 111 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 112 | # This is especially recommended for binary packages to ensure reproducibility, and is more 113 | # commonly ignored for libraries. 114 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 115 | #poetry.lock 116 | 117 | # pdm 118 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 119 | #pdm.lock 120 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 121 | # in version control. 122 | # https://pdm.fming.dev/#use-with-ide 123 | .pdm.toml 124 | 125 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 126 | __pypackages__/ 127 | 128 | # Celery stuff 129 | celerybeat-schedule 130 | celerybeat.pid 131 | 132 | # SageMath parsed files 133 | *.sage.py 134 | 135 | # Environments 136 | .env 137 | .venv 138 | env/ 139 | venv/ 140 | ENV/ 141 | env.bak/ 142 | venv.bak/ 143 | 144 | # Spyder project settings 145 | .spyderproject 146 | .spyproject 147 | 148 | # Rope project settings 149 | .ropeproject 150 | 151 | # mkdocs documentation 152 | /site 153 | 154 | # mypy 155 | .mypy_cache/ 156 | .dmypy.json 157 | dmypy.json 158 | 159 | # Pyre type checker 160 | .pyre/ 161 | 162 | # pytype static type analyzer 163 | .pytype/ 164 | 165 | # Cython debug symbols 166 | cython_debug/ 167 | 168 | # PyCharm 169 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 170 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 171 | # and can be added to the global gitignore or merged into this file. For a more nuclear 172 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 173 | #.idea/ 174 | -------------------------------------------------------------------------------- /sdist_upip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # This module is part of Pycopy https://github.com/pfalcon/pycopy 4 | # and pycopy-lib https://github.com/pfalcon/pycopy-lib, projects to 5 | # create a (very) lightweight full-stack Python distribution. 6 | # 7 | # Copyright (c) 2016-2019 Paul Sokolovsky 8 | # Licence: MIT 9 | # 10 | # This module overrides distutils (also compatible with setuptools) "sdist" 11 | # command to perform pre- and post-processing as required for MicroPython's 12 | # upip package manager. 13 | # 14 | # Preprocessing steps: 15 | # * Creation of Python resource module (R.py) from each top-level package's 16 | # resources. 17 | # Postprocessing steps: 18 | # * Removing metadata files not used by upip (this includes setup.py) 19 | # * Recompressing gzip archive with 4K dictionary size so it can be 20 | # installed even on low-heap targets. 21 | # 22 | import sys 23 | import os 24 | import zlib 25 | import tarfile 26 | import re 27 | import io 28 | 29 | from distutils.filelist import FileList 30 | from setuptools.command.sdist import sdist as _sdist 31 | 32 | 33 | FILTERS = [ 34 | # include, exclude, repeat 35 | (r".+\.egg-info/(PKG-INFO|requires\.txt)", r"setup.py$"), 36 | (r".+\.py$", r"[^/]+$"), 37 | (None, r".+\.egg-info/.+"), 38 | ] 39 | outbuf = io.BytesIO() 40 | 41 | 42 | def gzip_4k(inf, fname): 43 | comp = zlib.compressobj(level=9, wbits=16 + 12) 44 | with open(fname + ".out", "wb") as outf: 45 | while 1: 46 | data = inf.read(1024) 47 | if not data: 48 | break 49 | outf.write(comp.compress(data)) 50 | outf.write(comp.flush()) 51 | os.rename(fname, fname + ".orig") 52 | os.rename(fname + ".out", fname) 53 | 54 | 55 | def filter_tar(name): 56 | fin = tarfile.open(name, "r:gz") 57 | fout = tarfile.open(fileobj=outbuf, mode="w") 58 | for info in fin: 59 | # print(info) 60 | if not "/" in info.name: 61 | continue 62 | fname = info.name.split("/", 1)[1] 63 | include = None 64 | 65 | for inc_re, exc_re in FILTERS: 66 | if include is None and inc_re: 67 | if re.match(inc_re, fname): 68 | include = True 69 | 70 | if include is None and exc_re: 71 | if re.match(exc_re, fname): 72 | include = False 73 | 74 | if include is None: 75 | include = True 76 | 77 | if include: 78 | print("including:", fname) 79 | else: 80 | print("excluding:", fname) 81 | continue 82 | 83 | farch = fin.extractfile(info) 84 | fout.addfile(info, farch) 85 | fout.close() 86 | fin.close() 87 | 88 | 89 | def make_resource_module(manifest_files): 90 | resources = [] 91 | # Any non-python file included in manifest is resource 92 | for fname in manifest_files: 93 | ext = fname.rsplit(".", 1)[1] 94 | if ext != "py": 95 | resources.append(fname) 96 | 97 | if resources: 98 | print("creating resource module R.py") 99 | resources.sort() 100 | last_pkg = None 101 | r_file = None 102 | for fname in resources: 103 | try: 104 | pkg, res_name = fname.split("/", 1) 105 | except ValueError: 106 | print("not treating %s as a resource" % fname) 107 | continue 108 | if last_pkg != pkg: 109 | last_pkg = pkg 110 | if r_file: 111 | r_file.write("}\n") 112 | r_file.close() 113 | r_file = open(pkg + "/R.py", "w") 114 | r_file.write("R = {\n") 115 | 116 | with open(fname, "rb") as f: 117 | r_file.write("%r: %r,\n" % (res_name, f.read())) 118 | 119 | if r_file: 120 | r_file.write("}\n") 121 | r_file.close() 122 | 123 | 124 | class sdist(_sdist): 125 | 126 | def run(self): 127 | self.filelist = FileList() 128 | self.get_file_list() 129 | make_resource_module(self.filelist.files) 130 | 131 | r = super().run() 132 | 133 | assert len(self.archive_files) == 1 134 | print("filtering files and recompressing with 4K dictionary") 135 | filter_tar(self.archive_files[0]) 136 | outbuf.seek(0) 137 | gzip_4k(outbuf, self.archive_files[0]) 138 | 139 | return r 140 | 141 | 142 | # For testing only 143 | if __name__ == "__main__": 144 | filter_tar(sys.argv[1]) 145 | outbuf.seek(0) 146 | gzip_4k(outbuf, sys.argv[1]) 147 | -------------------------------------------------------------------------------- /examples/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """I2C LCD showcase""" 5 | 6 | from lcd_i2c import LCD 7 | from machine import I2C, Pin 8 | from time import sleep 9 | 10 | # PCF8574 on 0x27 11 | I2C_ADDR = 0x27 # DEC 39, HEX 0x27 12 | I2C_NUM_ROWS = 2 13 | I2C_NUM_COLS = 16 14 | FREQ = 800000 # Try lowering this value in case of "Errno 5" 15 | 16 | 17 | def print_and_wait(text: str, sleep_time: int = 2) -> None: 18 | """ 19 | Print to console and wait some time. 20 | 21 | :param text: The text to print to console 22 | :type text: str 23 | :param sleep_time: The sleep time in seconds 24 | :type sleep_time: int 25 | """ 26 | print(text) 27 | sleep(sleep_time) 28 | 29 | 30 | # define custom I2C interface, default is 'I2C(0)' 31 | # check the docs of your device for further details and pin infos 32 | # this are the pins for the Raspberry Pi Pico adapter board 33 | i2c = I2C(0, scl=Pin(13), sda=Pin(12), freq=FREQ) 34 | lcd = LCD(addr=I2C_ADDR, cols=I2C_NUM_COLS, rows=I2C_NUM_ROWS, i2c=i2c) 35 | 36 | # get LCD infos/properties 37 | print("LCD is on I2C address {}".format(lcd.addr)) 38 | print("LCD has {} columns and {} rows".format(lcd.cols, lcd.rows)) 39 | print("LCD is used with a charsize of {}".format(lcd.charsize)) 40 | print("Cursor position is {}".format(lcd.cursor_position)) 41 | 42 | # start LCD, not automatically called during init to be Arduino compatible 43 | lcd.begin() 44 | 45 | # print text on sceen at first row, starting on first column 46 | lcd.print("Hello World") 47 | print_and_wait("Show 'Hello World' on LCD") 48 | 49 | # turn LCD off 50 | lcd.no_backlight() 51 | print_and_wait("Turn LCD backlight off") 52 | 53 | # get current backlight value 54 | print("Backlight value: {}".format(lcd.get_backlight())) 55 | 56 | # turn LCD on 57 | lcd.backlight() 58 | print_and_wait("Turn LCD backlight on") 59 | 60 | # get current backlight value 61 | print("Backlight value: {}".format(lcd.get_backlight())) 62 | 63 | # clear LCD display content 64 | lcd.clear() 65 | print_and_wait("Clear display content") 66 | 67 | # turn cursor on (show) 68 | lcd.cursor() 69 | print_and_wait("Turn cursor on (show)") 70 | 71 | # blink cursor 72 | lcd.blink() 73 | print_and_wait("Blink cursor") 74 | 75 | # return cursor to home position 76 | lcd.home() 77 | print_and_wait("Return cursor to home position") 78 | 79 | # stop blinking cursor 80 | lcd.no_blink() 81 | print_and_wait("Stop blinking cursor") 82 | 83 | # turn cursor off (hide) 84 | lcd.no_cursor() 85 | print_and_wait("Turn cursor off (hide)") 86 | 87 | # print_and_wait text on sceen 88 | lcd.print("Hello again") 89 | print_and_wait("Show 'Hello again' on LCD") 90 | 91 | # turn display off 92 | lcd.no_display() 93 | print_and_wait("Turn LCD off") 94 | 95 | # turn display on 96 | lcd.display() 97 | print_and_wait("Turn LCD on") 98 | 99 | # scroll display to the left 100 | for _ in "Hello again": 101 | lcd.scroll_display_left() 102 | sleep(0.5) 103 | print_and_wait("Scroll display to the left") 104 | 105 | # scroll display to the right 106 | for _ in "Hello again": 107 | lcd.scroll_display_right() 108 | sleep(0.5) 109 | print_and_wait("Scroll display to the right") 110 | 111 | # set text flow right to left 112 | lcd.clear() 113 | lcd.set_cursor(col=12, row=0) 114 | lcd.right_to_left() 115 | lcd.print("Right to left") 116 | print_and_wait("Set text flow right to left") 117 | 118 | # set text flow left to right 119 | lcd.clear() 120 | lcd.set_cursor(col=0, row=0) 121 | lcd.left_to_right() 122 | lcd.print("Left to right") 123 | print_and_wait("Set text flow left to right") 124 | 125 | # activate autoscroll 126 | lcd.autoscroll() 127 | print_and_wait("Activate autoscroll") 128 | 129 | # disable autoscroll 130 | lcd.no_autoscroll() 131 | print_and_wait("Disable autoscroll") 132 | 133 | # set cursor to second line, seventh column 134 | lcd.clear() 135 | lcd.cursor() 136 | # lcd.cursor_position = (7, 1) 137 | lcd.set_cursor(col=7, row=1) 138 | print_and_wait("Set cursor to row 1, column 7") 139 | lcd.no_cursor() 140 | 141 | # set custom char number 0 as :-) 142 | # custom char can be set for location 0 ... 7 143 | lcd.create_char( 144 | location=0, 145 | charmap=[0x00, 0x00, 0x11, 0x04, 0x04, 0x11, 0x0E, 0x00] 146 | # this is the binary matrix, feel it, see it 147 | # 00000 148 | # 00000 149 | # 10001 150 | # 00100 151 | # 00100 152 | # 10001 153 | # 01110 154 | # 00000 155 | ) 156 | print_and_wait("Create custom char ':-)'") 157 | 158 | # show custom char stored at location 0 159 | lcd.print(chr(0)) 160 | lcd.print(chr(0)) 161 | print_and_wait("Show custom char") 162 | -------------------------------------------------------------------------------- /docs/EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Usage examples of this `micropython-i2c-lcd` library 4 | 5 | --------------- 6 | 7 | ## General 8 | 9 | An example of all implemented functionalities can be found at the 10 | [MicroPython I2C LCD examples folder][ref-micropython-i2c-lcd-examples] 11 | 12 | ## Setup Display 13 | 14 | ```python 15 | from lcd_i2c import LCD 16 | from machine import I2C, Pin 17 | 18 | # PCF8574 on 0x27 19 | I2C_ADDR = 0x27 20 | NUM_ROWS = 2 21 | NUM_COLS = 16 22 | 23 | # define custom I2C interface, default is 'I2C(0)' 24 | # check the docs of your device for further details and pin infos 25 | # this are the pins for the Raspberry Pi Pico adapter board 26 | i2c = I2C(0, scl=Pin(13), sda=Pin(12), freq=800000) 27 | lcd = LCD(addr=I2C_ADDR, cols=NUM_COLS, rows=NUM_ROWS, i2c=i2c) 28 | 29 | # get LCD infos/properties 30 | print("LCD is on I2C address {}".format(lcd.addr)) 31 | print("LCD has {} columns and {} rows".format(lcd.cols, lcd.rows)) 32 | print("LCD is used with a charsize of {}".format(lcd.charsize)) 33 | print("Cursor position is {}".format(lcd.cursor_position)) 34 | 35 | # start LCD, not automatically called during init to be Arduino compatible 36 | lcd.begin() 37 | ``` 38 | 39 | ## Text 40 | 41 | ### Show Text 42 | 43 | ```python 44 | # LCD has already been setup, see section "Setup Display" 45 | 46 | lcd.print("Hello World") 47 | ``` 48 | 49 | ### Clear Text 50 | 51 | This command clears the text on the screen and sets the cursor position back 52 | to its home position at `(0, 0)` 53 | 54 | ```python 55 | # LCD has already been setup, see section "Setup Display" 56 | 57 | lcd.clear() 58 | ``` 59 | 60 | ### Scroll Text 61 | 62 | ```python 63 | # LCD has already been setup, see section "Setup Display" 64 | from time import sleep 65 | 66 | text = "Hello World" 67 | 68 | # show text on LCD 69 | lcd.print(text) 70 | 71 | # scroll text to the left 72 | for _ in text: 73 | lcd.scroll_display_left() 74 | sleep(0.5) 75 | 76 | # scroll text to the right 77 | for _ in text: 78 | lcd.scroll_display_right() 79 | sleep(0.5) 80 | ``` 81 | 82 | ### Text Flow 83 | 84 | ```python 85 | # LCD has already been setup, see section "Setup Display" 86 | 87 | # set text flow right to left 88 | lcd.set_cursor(col=12, row=0) 89 | lcd.right_to_left() 90 | lcd.print("Right to left") 91 | 92 | # set text flow left to right 93 | lcd.set_cursor(col=0, row=0) 94 | lcd.left_to_right() 95 | lcd.print("Left to right") 96 | ``` 97 | 98 | ### Autoscroll 99 | 100 | ```python 101 | # LCD has already been setup, see section "Setup Display" 102 | 103 | # activate autoscroll 104 | lcd.autoscroll() 105 | 106 | # disable autoscroll 107 | lcd.no_autoscroll() 108 | ``` 109 | 110 | ### Custom Characters 111 | 112 | Custom characters can be defined for 8 CGRAM locations. The character has to 113 | be defined as binary of HEX list. In case you can't see the matrix, simply use 114 | the [LCD Character Creator page of Max Promer](https://maxpromer.github.io/LCD-Character-Creator/) 115 | 116 | The following example defines a upright happy smiley `:-)` at the first (0) 117 | location in the displays CGRAM using 5x10 pixels. Maybe you can see it ... 118 | 119 | ``` 120 | 00000 121 | 00000 122 | 10001 123 | 00100 124 | 00100 125 | 10001 126 | 01110 127 | 00000 128 | ``` 129 | 130 | ```python 131 | # LCD has already been setup, see section "Setup Display" 132 | 133 | # custom char can be set for location 0 ... 7 134 | lcd.create_char( 135 | location=0, 136 | charmap=[0x00, 0x00, 0x11, 0x04, 0x04, 0x11, 0x0E, 0x00] 137 | ) 138 | 139 | # show custom char stored at location 0 140 | lcd.print(chr(0)) 141 | ``` 142 | 143 | ## Backlight 144 | 145 | The following functions can be used to control the LCD backlight 146 | 147 | ```python 148 | # LCD has already been setup, see section "Setup Display" 149 | 150 | # turn LCD off 151 | lcd.no_backlight() 152 | 153 | # turn LCD on 154 | lcd.backlight() 155 | 156 | # turn LCD off 157 | lcd.set_backlight(False) 158 | 159 | # turn LCD on 160 | lcd.set_backlight(True) 161 | 162 | # get current backlight value 163 | print("Backlight value: {}".format(lcd.get_backlight())) 164 | 165 | # get current backlight value via property 166 | print("Backlight value: {}".format(lcd.backlightval)) 167 | ``` 168 | 169 | ## Cursor 170 | 171 | The following functions can be used to control the cursor 172 | 173 | ```python 174 | # LCD has already been setup, see section "Setup Display" 175 | 176 | # turn cursor on (show) 177 | lcd.cursor() 178 | 179 | # turn cursor off (hide) 180 | lcd.no_cursor() 181 | 182 | # turn cursor on (show) 183 | lcd.cursor_on() 184 | 185 | # turn cursor off (hide) 186 | lcd.cursor_off() 187 | 188 | # blink cursor 189 | lcd.blink() 190 | 191 | # stop blinking cursor 192 | lcd.no_blink() 193 | 194 | # set cursor to home position (0, 0) 195 | lcd.home() 196 | 197 | # set cursor position to first line, third column 198 | lcd.set_cursor(col=3, row=0) 199 | 200 | # set cursor position to second line, seventh column 201 | lcd.cursor_position = (7, 1) 202 | 203 | # get current cursor position via property 204 | print("Cursor position: {}".format(lcd.cursor_position)) 205 | ``` 206 | 207 | ## Display 208 | 209 | ```python 210 | # LCD has already been setup, see section "Setup Display" 211 | 212 | # turn display off 213 | lcd.no_display() 214 | 215 | # turn display on 216 | lcd.display() 217 | ``` 218 | 219 | 220 | [ref-micropython-i2c-lcd-examples]: https://github.com/brainelectronics/micropython-i2c-lcd/tree/develop/examples 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroPython I2C LCD 2 | 3 | [![Downloads](https://pepy.tech/badge/micropython-i2c-lcd)](https://pepy.tech/project/micropython-i2c-lcd) 4 | ![Release](https://img.shields.io/github/v/release/brainelectronics/micropython-i2c-lcd?include_prereleases&color=success) 5 | ![MicroPython](https://img.shields.io/badge/micropython-Ok-green.svg) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![codecov](https://codecov.io/github/brainelectronics/micropython-i2c-lcd/branch/main/graph/badge.svg)](https://app.codecov.io/github/brainelectronics/micropython-i2c-lcd) 8 | [![CI](https://github.com/brainelectronics/micropython-i2c-lcd/actions/workflows/release.yml/badge.svg)](https://github.com/brainelectronics/micropython-i2c-lcd/actions/workflows/release.yml) 9 | 10 | MicroPython package to control HD44780 LCD displays 1602 and 2004 via I2C 11 | 12 | --------------- 13 | 14 | ## General 15 | 16 | MicroPython package to control HD44780 LCD displays 1602 and 2004 via I2C 17 | 18 | 📚 The latest documentation is available at 19 | [MicroPython I2C LCD ReadTheDocs][ref-rtd-micropython-i2c-lcd] 📚 20 | 21 | 22 | 23 | - [Installation](#installation) 24 | - [Install required tools](#install-required-tools) 25 | - [Setup](#setup) 26 | - [Install package with upip](#install-package-with-upip) 27 | - [General](#general) 28 | - [Specific version](#specific-version) 29 | - [Test version](#test-version) 30 | - [Manually](#manually) 31 | - [Upload files to board](#upload-files-to-board) 32 | - [Usage](#usage) 33 | - [Credits](#credits) 34 | 35 | 36 | 37 | ## Installation 38 | 39 | ### Install required tools 40 | 41 | Python3 must be installed on your system. Check the current Python version 42 | with the following command 43 | 44 | ```bash 45 | python --version 46 | python3 --version 47 | ``` 48 | 49 | Depending on which command `Python 3.x.y` (with x.y as some numbers) is 50 | returned, use that command to proceed. 51 | 52 | ```bash 53 | python3 -m venv .venv 54 | source .venv/bin/activate 55 | 56 | pip install -r requirements.txt 57 | ``` 58 | 59 | ## Setup 60 | 61 | ### Install package with upip 62 | 63 | Connect the MicroPython device to a network (if possible) 64 | 65 | ```python 66 | import network 67 | station = network.WLAN(network.STA_IF) 68 | station.active(True) 69 | station.connect('SSID', 'PASSWORD') 70 | station.isconnected() 71 | ``` 72 | 73 | #### General 74 | 75 | Install the latest package version of this lib on the MicroPython device 76 | 77 | ```python 78 | import mip 79 | mip.install("github:brainelectronics/micropython-i2c-lcd") 80 | ``` 81 | 82 | For MicroPython versions below 1.19.1 use the `upip` package instead of `mip` 83 | 84 | ```python 85 | import upip 86 | upip.install('micropython-i2c-lcd') 87 | ``` 88 | 89 | #### Specific version 90 | 91 | Install a specific, fixed package version of this lib on the MicroPython device 92 | 93 | ```python 94 | import mip 95 | # install a verions of a specific branch 96 | mip.install("github:brainelectronics/micropython-i2c-lcd", version="feature/initial-implementation") 97 | # install a tag version 98 | mip.install("github:brainelectronics/micropython-i2c-lcd", version="0.1.0") 99 | ``` 100 | 101 | For MicroPython versions below 1.19.1 use the `upip` package instead of `mip` 102 | 103 | ```python 104 | import upip 105 | upip.install('micropython-i2c-lcd==0.1.0') 106 | ``` 107 | 108 | #### Test version 109 | 110 | Install a specific release candidate version uploaded to 111 | [Test Python Package Index](https://test.pypi.org/) on every PR on the 112 | MicroPython device. If no specific version is set, the latest stable version 113 | will be used. 114 | 115 | ```python 116 | import mip 117 | mip.install("github:brainelectronics/micropython-i2c-lcd", version="0.1.0-rc3.dev1") 118 | ``` 119 | 120 | For MicroPython versions below 1.19.1 use the `upip` package instead of `mip` 121 | 122 | ```python 123 | import upip 124 | # overwrite index_urls to only take artifacts from test.pypi.org 125 | upip.index_urls = ['https://test.pypi.org/pypi'] 126 | upip.install('micropython-i2c-lcd==0.1.0rc3.dev1') 127 | ``` 128 | 129 | ### Manually 130 | 131 | #### Upload files to board 132 | 133 | Copy the module to the MicroPython board and import them as shown below 134 | using [Remote MicroPython shell][ref-remote-upy-shell] 135 | 136 | Open the remote shell with the following command. Additionally use `-b 115200` 137 | in case no CP210x is used but a CH34x. 138 | 139 | ```bash 140 | rshell --port /dev/tty.SLAB_USBtoUART --editor nano 141 | ``` 142 | 143 | Perform the following command inside the `rshell` to copy all files and 144 | folders to the device 145 | 146 | ```bash 147 | mkdir /pyboard/lib 148 | mkdir /pyboard/lib/lcd_i2c 149 | 150 | cp lcd_i2c/* /pyboard/lib/lcd_i2c 151 | 152 | cp examples/main.py /pyboard 153 | cp examples/boot.py /pyboard 154 | ``` 155 | 156 | ## Usage 157 | 158 | ```python 159 | from lcd_i2c import LCD 160 | from machine import I2C, Pin 161 | 162 | # PCF8574 on 0x50 163 | I2C_ADDR = 0x27 # DEC 39, HEX 0x27 164 | NUM_ROWS = 2 165 | NUM_COLS = 16 166 | 167 | # define custom I2C interface, default is 'I2C(0)' 168 | # check the docs of your device for further details and pin infos 169 | i2c = I2C(0, scl=Pin(13), sda=Pin(12), freq=800000) 170 | lcd = LCD(addr=I2C_ADDR, cols=NUM_COLS, rows=NUM_ROWS, i2c=i2c) 171 | 172 | lcd.begin() 173 | lcd.print("Hello World") 174 | ``` 175 | 176 | For further examples check the `examples` folder or the Example chapter in the 177 | docs. 178 | 179 | ## Credits 180 | 181 | Based on [Frank de Brabanders Arduino LiquidCrystal I2C Library][ref-arduino-lcd-i2c-library]. 182 | 183 | 184 | [ref-rtd-micropython-i2c-lcd]: https://micropython-i2c-lcd.readthedocs.io/en/latest/ 185 | [ref-remote-upy-shell]: https://github.com/dhylands/rshell 186 | [ref-arduino-lcd-i2c-library]: https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library 187 | [ref-test-pypi]: https://test.pypi.org/ 188 | [ref-pypi]: https://pypi.org/ 189 | -------------------------------------------------------------------------------- /tests/test_lcd_i2c.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Unittest for MicroPython I2C LCD""" 5 | 6 | import logging 7 | from unittest.mock import Mock, patch 8 | from nose2.tools import params 9 | import sys 10 | import unittest 11 | 12 | 13 | class Pin(object): 14 | """Fake MicroPython Pin class""" 15 | def __init__(self, pin: int, mode: int = -1): 16 | self._pin = pin 17 | self._mode = mode 18 | self._value = 0 19 | 20 | 21 | class I2C(object): 22 | """Fake MicroPython I2C class""" 23 | def __init__(self, id: int, *, scl: Pin, sda: Pin, freq: int = 400000): 24 | self._id = id 25 | self._scl = scl 26 | self._sda = sda 27 | self._freq = freq 28 | 29 | def writeto(addr: int, buf: bytearray, stop: bool = True) -> int: 30 | return 1 31 | 32 | 33 | # custom imports 34 | sys.modules['machine.I2C'] = I2C 35 | to_be_mocked = [ 36 | 'machine', 37 | 'time.sleep_ms', 'time.sleep_us', 38 | ] 39 | for module in to_be_mocked: 40 | sys.modules[module] = Mock() 41 | 42 | from lcd_i2c import LCD # noqa: E402 43 | from lcd_i2c import const as Const # noqa: E402 44 | 45 | 46 | class TestLCD(unittest.TestCase): 47 | """This class describes a TestLCD unittest.""" 48 | 49 | def setUp(self) -> None: 50 | """Run before every test method""" 51 | # define a format 52 | custom_format = "[%(asctime)s][%(levelname)-8s][%(filename)-20s @" \ 53 | " %(funcName)-15s:%(lineno)4s] %(message)s" 54 | 55 | # set basic config and level for all loggers 56 | logging.basicConfig(level=logging.INFO, 57 | format=custom_format, 58 | stream=sys.stdout) 59 | 60 | # create a logger for this TestSuite 61 | self.test_logger = logging.getLogger(__name__) 62 | 63 | # set the test logger level 64 | self.test_logger.setLevel(logging.DEBUG) 65 | 66 | # enable/disable the log output of the device logger for the tests 67 | # if enabled log data inside this test will be printed 68 | self.test_logger.disabled = False 69 | 70 | self.i2c = I2C(1, scl=Pin(3), sda=Pin(2), freq=800_000) 71 | self._calls_counter = 0 72 | self._tracked_call_data: list = [] 73 | 74 | def _tracked_call(self, *args, **kwargs) -> None: 75 | """Track function calls and the used arguments""" 76 | self._tracked_call_data.append({'args': args, 'kwargs': kwargs}) 77 | 78 | @params( 79 | (0x27), 80 | (39), 81 | ) 82 | def test_addr(self, addr: int) -> None: 83 | """Test address property""" 84 | lcd = LCD(addr=addr, cols=16, rows=2, i2c=self.i2c) 85 | 86 | self.assertEqual(lcd.addr, addr) 87 | 88 | @params( 89 | (16), 90 | (20), 91 | ) 92 | def test_cols(self, cols: int) -> None: 93 | """Test columns property""" 94 | lcd = LCD(addr=0x27, cols=cols, rows=2, i2c=self.i2c) 95 | 96 | self.assertEqual(lcd.cols, cols) 97 | 98 | @params( 99 | (1), 100 | (2), 101 | (4), 102 | ) 103 | def test_rows(self, rows: int) -> None: 104 | """Test rows property""" 105 | lcd = LCD(addr=0x27, cols=16, rows=rows, i2c=self.i2c) 106 | 107 | self.assertEqual(lcd.rows, rows) 108 | 109 | def test_charsize(self) -> None: 110 | """Test charsize property""" 111 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 112 | self.assertEqual(lcd.charsize, 0) 113 | 114 | lcd = LCD(addr=0x27, cols=16, rows=2, charsize=1, i2c=self.i2c) 115 | self.assertEqual(lcd.charsize, 1) 116 | 117 | def test_backlightval(self) -> None: 118 | """Test backlightval property""" 119 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 120 | 121 | # active by default 122 | self.assertEqual(lcd.backlightval, Const.LCD_BACKLIGHT) 123 | self.assertTrue(lcd.get_backlight()) 124 | 125 | lcd.no_backlight() 126 | self.assertEqual(lcd.backlightval, Const.LCD_NOBACKLIGHT) 127 | self.assertFalse(lcd.get_backlight()) 128 | 129 | lcd.backlight() 130 | self.assertEqual(lcd.backlightval, Const.LCD_BACKLIGHT) 131 | self.assertTrue(lcd.get_backlight()) 132 | 133 | lcd.set_backlight(new_val=False) 134 | self.assertEqual(lcd.backlightval, Const.LCD_NOBACKLIGHT) 135 | self.assertFalse(lcd.get_backlight()) 136 | 137 | lcd.set_backlight(new_val=True) 138 | self.assertEqual(lcd.backlightval, Const.LCD_BACKLIGHT) 139 | self.assertTrue(lcd.get_backlight()) 140 | 141 | def test_cursor_position(self) -> None: 142 | """Test cursor position property""" 143 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 144 | self.assertEqual(lcd.cursor_position, (0, 0)) 145 | 146 | # test valid cursor position 147 | lcd.cursor_position = (10, 1) 148 | self.assertEqual(lcd.cursor_position, (10, 1)) 149 | 150 | # test invald cursor position (not enough rows) 151 | lcd.cursor_position = (10, 2) 152 | self.assertEqual(lcd.cursor_position, (10, 1)) 153 | 154 | def test_begin(self) -> None: 155 | """Test LCD begin""" 156 | # default I2C interface 157 | lcd = LCD(addr=0x27, cols=16, rows=2) 158 | lcd.begin() 159 | 160 | # self.assertEqual(lcd._i2c._id, 0) 161 | self.assertEqual(lcd._display_function, 0x8) 162 | self.assertEqual(lcd._display_control, 0x4) 163 | self.assertEqual(lcd._display_mode, 2) 164 | self.assertEqual(lcd.cursor_position, (0, 0)) 165 | 166 | # double row display 167 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 168 | lcd.begin() 169 | 170 | # self.assertEqual(lcd._i2c._id, self.i2c._id) 171 | self.assertEqual(lcd._display_function, 0x8) 172 | self.assertEqual(lcd._display_control, 0x4) 173 | self.assertEqual(lcd._display_mode, 2) 174 | self.assertEqual(lcd.cursor_position, (0, 0)) 175 | 176 | # single row display 177 | lcd = LCD(addr=0x27, cols=16, rows=1, i2c=self.i2c) 178 | lcd.begin() 179 | 180 | # self.assertEqual(lcd._i2c._id, self.i2c._id) 181 | self.assertEqual(lcd._display_function, 0x0) 182 | self.assertEqual(lcd._display_control, 0x4) 183 | self.assertEqual(lcd._display_mode, 2) 184 | self.assertEqual(lcd.cursor_position, (0, 0)) 185 | 186 | # single row display with different char size 187 | lcd = LCD(addr=0x27, cols=16, rows=1, charsize=0x1, i2c=self.i2c) 188 | lcd.begin() 189 | 190 | # self.assertEqual(lcd._i2c._id, self.i2c._id) 191 | self.assertEqual(lcd._display_function, 0x4) 192 | self.assertEqual(lcd._display_control, 0x4) 193 | self.assertEqual(lcd._display_mode, 2) 194 | self.assertEqual(lcd.cursor_position, (0, 0)) 195 | 196 | def test_clear(self) -> None: 197 | """Test clear display""" 198 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 199 | lcd.set_cursor(col=3, row=1) 200 | 201 | lcd.cursor_position = (3, 1) 202 | lcd.clear() 203 | lcd.cursor_position = (0, 0) 204 | 205 | def test_home(self) -> None: 206 | """Test home display""" 207 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 208 | lcd.set_cursor(col=3, row=1) 209 | 210 | lcd.cursor_position = (3, 1) 211 | lcd.home() 212 | lcd.cursor_position = (0, 0) 213 | 214 | def test_display(self) -> None: 215 | """Test display on/off functions""" 216 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 217 | lcd.begin() 218 | self.assertEqual(lcd._display_control, 0x4) 219 | 220 | lcd.no_display() 221 | self.assertEqual(lcd._display_control, 0x0) 222 | 223 | lcd.display() 224 | self.assertEqual(lcd._display_control, 0x4) 225 | 226 | def test_blink(self) -> None: 227 | """Test cursor blink on/off functions""" 228 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 229 | lcd.begin() 230 | self.assertEqual(lcd._display_control, 0x4) 231 | 232 | lcd.blink() 233 | self.assertEqual(lcd._display_control, 0x5) 234 | 235 | lcd.no_blink() 236 | self.assertEqual(lcd._display_control, 0x4) 237 | 238 | lcd.blink_on() 239 | self.assertEqual(lcd._display_control, 0x5) 240 | 241 | lcd.blink_off() 242 | self.assertEqual(lcd._display_control, 0x4) 243 | 244 | def test_cursor(self) -> None: 245 | """Test cursor on/off functions""" 246 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 247 | lcd.begin() 248 | self.assertEqual(lcd._display_control, 0x4) 249 | 250 | lcd.cursor() 251 | self.assertEqual(lcd._display_control, 0x6) 252 | 253 | lcd.no_cursor() 254 | self.assertEqual(lcd._display_control, 0x4) 255 | 256 | lcd.cursor_on() 257 | self.assertEqual(lcd._display_control, 0x6) 258 | 259 | lcd.cursor_off() 260 | self.assertEqual(lcd._display_control, 0x4) 261 | 262 | lcd.cursor() 263 | lcd.blink() 264 | self.assertEqual(lcd._display_control, 0x7) 265 | 266 | lcd.no_blink() 267 | self.assertEqual(lcd._display_control, 0x6) 268 | 269 | lcd.no_cursor() 270 | self.assertEqual(lcd._display_control, 0x4) 271 | 272 | def test_scroll_display_left(self) -> None: 273 | """Test scrolling test to the left""" 274 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 275 | lcd.begin() 276 | 277 | with patch('lcd_i2c.LCD._command', wraps=self._tracked_call): 278 | lcd.scroll_display_left() 279 | 280 | self.assertEqual(len(self._tracked_call_data), 1) 281 | self.assertEqual(self._tracked_call_data[0]['kwargs']['value'], 0x18) 282 | 283 | def test_scroll_display_right(self) -> None: 284 | """Test scrolling test to the right""" 285 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 286 | lcd.begin() 287 | 288 | with patch('lcd_i2c.LCD._command', wraps=self._tracked_call): 289 | lcd.scroll_display_right() 290 | 291 | self.assertEqual(len(self._tracked_call_data), 1) 292 | self.assertEqual(self._tracked_call_data[0]['kwargs']['value'], 0x1C) 293 | 294 | def test_text_flow(self) -> None: 295 | """Test setting text flow left to right""" 296 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 297 | lcd.begin() 298 | self.assertEqual(lcd._display_mode, 0x2) 299 | 300 | lcd.right_to_left() 301 | self.assertEqual(lcd._display_mode, 0x0) 302 | 303 | lcd.left_to_right() 304 | self.assertEqual(lcd._display_mode, 0x2) 305 | 306 | def test_autoscroll(self) -> None: 307 | """Test autoscroll function""" 308 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 309 | lcd.begin() 310 | self.assertEqual(lcd._display_mode, 0x2) 311 | 312 | lcd.autoscroll() 313 | self.assertEqual(lcd._display_mode, 0x3) 314 | 315 | lcd.no_autoscroll() 316 | self.assertEqual(lcd._display_mode, 0x2) 317 | 318 | def test_create_char(self) -> None: 319 | """Test creating custom char""" 320 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 321 | lcd.begin() 322 | 323 | charmap = [ 324 | 0x0, # 00000 325 | 0x1, # 00001 326 | 0x3, # 00011 327 | 0x7, # 00111 328 | 0xF, # 01111 329 | 0x1F, # 11111 330 | 0x4, # 00100 331 | 0x11, # 10001 332 | ] 333 | 334 | with patch('lcd_i2c.LCD._command', wraps=self._tracked_call): 335 | lcd.create_char(location=0, charmap=charmap) 336 | 337 | self.assertEqual(len(self._tracked_call_data) - 1, len(charmap)) 338 | 339 | # command value to set CGRAM address at the given location 340 | self.assertEqual(self._tracked_call_data.pop(0)['kwargs']['value'], 64) 341 | 342 | # check char map send command content 343 | for idx, val in enumerate(charmap): 344 | self.assertEqual(self._tracked_call_data[idx]['kwargs']['value'], 345 | val) 346 | 347 | def test_print(self) -> None: 348 | """Test print on LCD""" 349 | lcd = LCD(addr=0x27, cols=16, rows=2, i2c=self.i2c) 350 | lcd.begin() 351 | self.assertEqual(lcd.cursor_position, (0, 0)) 352 | 353 | text = "Hello" 354 | 355 | with patch('lcd_i2c.LCD._command', wraps=self._tracked_call): 356 | lcd.print(text) 357 | 358 | # called 1x to much 359 | self.assertEqual(len(self._tracked_call_data) - 1, len(text)) 360 | self.assertEqual(lcd.cursor_position, (0 + len(text), 0)) 361 | 362 | for idx, val in enumerate(text): 363 | self.assertEqual(self._tracked_call_data[idx]['kwargs']['value'], 364 | ord(val)) 365 | 366 | def tearDown(self) -> None: 367 | """Run after every test method""" 368 | pass 369 | 370 | 371 | if __name__ == '__main__': 372 | unittest.main() 373 | -------------------------------------------------------------------------------- /lcd_i2c/lcd_i2c.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """ 5 | I2C LCD Display driver for 1602 and 2004 displays controlled via I2C 6 | 7 | LCD data sheet: https://www.sparkfun.com/datasheets/LCD/HD44780.pdf 8 | 9 | Ported to MicroPython from 10 | https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library 11 | """ 12 | 13 | # system packages 14 | from machine import I2C 15 | from time import sleep, sleep_ms, sleep_us 16 | 17 | # custom packages 18 | from . import const as Const 19 | 20 | # typing not natively supported on MicroPython 21 | from .typing import List, Optional, Tuple, Union 22 | 23 | 24 | class LCD: 25 | """Driver for the Liquid Crystal LCD displays that use the I2C bus""" 26 | 27 | def __init__(self, 28 | addr: int, 29 | cols: int, 30 | rows: int, 31 | charsize: int = 0x00, 32 | i2c: Optional[I2C] = None) -> None: 33 | """ 34 | Constructs a new instance. 35 | 36 | :param addr: The LCD I2C bus address 37 | :type addr: int 38 | :param cols: Number of columns of the LCD 39 | :type cols: int 40 | :param rows: Number of rows of the LCD 41 | :type rows: int 42 | :param charsize: The size in dots of the LCD 43 | :type charsize: int 44 | :param i2c: I2C object 45 | :type i2c: I2C 46 | """ 47 | self._addr: int = addr 48 | self._cols: int = cols 49 | self._rows: int = rows 50 | self._charsize: int = charsize 51 | self._backlightval: int = Const.LCD_BACKLIGHT 52 | if i2c is None: 53 | # default assignment, check the docs 54 | self._i2c = I2C(0) 55 | else: 56 | self._i2c = i2c 57 | 58 | self._display_control: int = 0 59 | self._display_mode: int = 0 60 | self._display_function: int = 0 61 | self._cursor_position: Tuple[int, int] = (0, 0) # (x, y) 62 | 63 | @property 64 | def addr(self) -> int: 65 | """ 66 | Get the LCD I2C bus address 67 | 68 | :returns: LCD I2C bus address 69 | :rtype: int 70 | """ 71 | return self._addr 72 | 73 | @property 74 | def cols(self) -> int: 75 | """ 76 | Get the number of columns of the LCD 77 | 78 | :returns: Number of columns of the LCD 79 | :rtype: int 80 | """ 81 | return self._cols 82 | 83 | @property 84 | def rows(self) -> int: 85 | """ 86 | Get the number of rows of the LCD 87 | 88 | :returns: Number of rows of the LCD 89 | :rtype: int 90 | """ 91 | return self._rows 92 | 93 | @property 94 | def charsize(self) -> int: 95 | """ 96 | Get the size in dots of the LCD 97 | 98 | :returns: Dot size of the LCD 99 | :rtype: int 100 | """ 101 | return self._charsize 102 | 103 | @property 104 | def backlightval(self) -> int: 105 | """ 106 | Get the backlight value 107 | 108 | :returns: Backlight value of the LCD 109 | :rtype: int 110 | """ 111 | return self._backlightval 112 | 113 | @property 114 | def cursor_position(self) -> Tuple[int, int]: 115 | """ 116 | Get the current cursor position 117 | 118 | :returns: Cursor position as tuple(column, row) as (x, y) 119 | :rtype: Tuple[int, int] 120 | """ 121 | return self._cursor_position 122 | 123 | @cursor_position.setter 124 | def cursor_position(self, position: Tuple[int, int]) -> None: 125 | """ 126 | Set the cursor position 127 | 128 | :param position: The cursor position 129 | :type position: Tuple[int, int] 130 | """ 131 | self.set_cursor(col=position[0], row=position[1]) # (x, y) 132 | 133 | def begin(self) -> None: 134 | """ 135 | Set the LCD display in the correct begin state 136 | 137 | Must be called before anything else is done 138 | """ 139 | self._display_function = \ 140 | Const.LCD_4BITMODE | Const.LCD_1LINE | Const.LCD_5x8DOTS 141 | 142 | if self.rows > 1: 143 | self._display_function |= Const.LCD_2LINE 144 | 145 | # for some 1 line displays you can select a 10 pixel high font 146 | if (self.charsize != 0) and (self.rows == 1): 147 | self._display_function |= Const.LCD_5x10DOTS 148 | 149 | # SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION! 150 | # according to datasheet, we need at least 40ms after power rises 151 | # above 2.7V before sending commands. Controller can turn on way before 152 | # 4.5V so we'll wait 50ms 153 | sleep_ms(50) 154 | 155 | # Now we pull both RS and R/W low to begin commands 156 | # reset expanderand turn backlight off (Bit 8 =1) 157 | self._expander_write(value=self.backlightval) 158 | sleep(1) 159 | 160 | # put the LCD into 4 bit mode 161 | # this is according to the Hitachi HD44780 datasheet 162 | # figure 24, page 46 163 | 164 | # we start in 8 bit mode, try to set 4 bit mode 165 | for _ in range(0, 3): 166 | self._write_4_bits(value=(0x03 << 4)) 167 | sleep_us(4500) # wait minimum 4.1ms 168 | 169 | # finally, set to 4 bit interface 170 | self._write_4_bits(value=(0x02 << 4)) 171 | 172 | # set number of lines, font size, etc 173 | self._command(value=(Const.LCD_FUNCTIONSET | self._display_function)) 174 | 175 | # turn the display on with no cursor or blinking default 176 | self._display_control = \ 177 | Const.LCD_DISPLAYON | Const.LCD_CURSOROFF | Const.LCD_BLINKOFF 178 | self.display() 179 | 180 | # clear it off 181 | self.clear() 182 | 183 | # Initialize to default text direction (for roman languages) 184 | self._display_mode = \ 185 | Const.LCD_ENTRYLEFT | Const.LCD_ENTRYSHIFTDECREMENT 186 | 187 | # set the entry mode 188 | self._command(value=(Const.LCD_ENTRYMODESET | self._display_mode)) 189 | 190 | self.home() 191 | 192 | def clear(self) -> None: 193 | """ 194 | Remove all the characters currently shown 195 | 196 | Next print/write operation will start from the first position on LCD 197 | display. 198 | """ 199 | # clear display and set cursor position to zero 200 | self._command(value=Const.LCD_CLEARDISPLAY) 201 | sleep_ms(2) # this command takes a long time! 202 | self._cursor_position = (0, 0) # (x, y) 203 | 204 | def home(self) -> None: 205 | """ 206 | Set cursor to home position (0, 0) 207 | 208 | Next print/write operation will start from the first position on the 209 | LCD display. 210 | """ 211 | # set cursor position to zero 212 | self._command(value=Const.LCD_RETURNHOME) 213 | sleep_ms(2) # this command takes a long time! 214 | self._cursor_position = (0, 0) # (x, y) 215 | 216 | def no_display(self) -> None: 217 | """ 218 | Turn the display off 219 | 220 | Do not show any characters on the LCD display. Backlight state will 221 | remain unchanged. Also all characters written on the display will 222 | return, when the display in enabled again. 223 | 224 | @see display 225 | """ 226 | self._display_control &= ~Const.LCD_DISPLAYON 227 | self._command(value=(Const.LCD_DISPLAYCONTROL | self._display_control)) 228 | 229 | def display(self) -> None: 230 | """ 231 | Turn the display on 232 | 233 | Show the characters on the LCD display, this is the normal behaviour. 234 | This method should only be used after no_display() has been used. 235 | 236 | @see no_display 237 | """ 238 | self._display_control |= Const.LCD_DISPLAYON 239 | self._command(value=(Const.LCD_DISPLAYCONTROL | self._display_control)) 240 | 241 | def no_blink(self) -> None: 242 | """Turn the blinking cursor off""" 243 | self._display_control &= ~Const.LCD_BLINKON 244 | self._command(value=(Const.LCD_DISPLAYCONTROL | self._display_control)) 245 | 246 | def blink(self) -> None: 247 | """Turn the blinking cursor on""" 248 | self._display_control |= Const.LCD_BLINKON 249 | self._command(value=(Const.LCD_DISPLAYCONTROL | self._display_control)) 250 | 251 | def blink_on(self) -> None: 252 | """ 253 | Turn on blinking cursor 254 | 255 | @see blink 256 | """ 257 | self.blink() 258 | 259 | def blink_off(self) -> None: 260 | """ 261 | Turn off blinking cursor 262 | 263 | @see no_blink 264 | """ 265 | self.no_blink() 266 | 267 | def no_cursor(self) -> None: 268 | """Turn the underline cursor off""" 269 | self._display_control &= ~Const.LCD_CURSORON 270 | self._command(value=(Const.LCD_DISPLAYCONTROL | self._display_control)) 271 | 272 | def cursor(self) -> None: 273 | """ 274 | Turn the underline cursor on 275 | 276 | Cursor can blink or not blink. Use the methods @see blink and 277 | @see no_blink for changing the cursor blink status. 278 | """ 279 | self._display_control |= Const.LCD_CURSORON 280 | self._command(value=(Const.LCD_DISPLAYCONTROL | self._display_control)) 281 | 282 | def cursor_on(self) -> None: 283 | """ 284 | Show cursor 285 | 286 | @see cursor 287 | """ 288 | self.cursor() 289 | 290 | def cursor_off(self) -> None: 291 | """ 292 | Hide cursor 293 | 294 | @see no_cursor 295 | """ 296 | self.no_cursor() 297 | 298 | def set_cursor(self, col: int, row: int) -> None: 299 | """ 300 | Set the cursor 301 | 302 | :param col: The new column of the cursor 303 | :type col: int 304 | :param row: The new row of the cursor 305 | :type row: int 306 | """ 307 | row_offsets: List[int] = [0x00, 0x40, 0x14, 0x54] 308 | 309 | # we count rows starting w/0 310 | if row > (self.rows - 1): 311 | row = self.rows - 1 312 | 313 | self._command( 314 | value=(Const.LCD_SETDDRAMADDR | (col + row_offsets[row])) 315 | ) 316 | 317 | self._cursor_position = (col, row) # (x, y) 318 | 319 | def scroll_display_left(self) -> None: 320 | """Scroll the display to the left by one""" 321 | self._command(value=(Const.LCD_CURSORSHIFT | Const.LCD_DISPLAYMOVE | Const.LCD_MOVELEFT)) # noqa: E501 322 | 323 | def scroll_display_right(self) -> None: 324 | """Scroll the display to the right by one""" 325 | self._command(value=(Const.LCD_CURSORSHIFT | Const.LCD_DISPLAYMOVE | Const.LCD_MOVERIGHT)) # noqa: E501 326 | 327 | def left_to_right(self) -> None: 328 | """Set text flow left to right""" 329 | self._display_mode |= Const.LCD_ENTRYLEFT 330 | self._command(value=(Const.LCD_ENTRYMODESET | self._display_mode)) 331 | 332 | def right_to_left(self) -> None: 333 | """Set text flow right to left""" 334 | self._display_mode &= ~Const.LCD_ENTRYLEFT 335 | self._command(value=(Const.LCD_ENTRYMODESET | self._display_mode)) 336 | 337 | def no_backlight(self) -> None: 338 | """Turn backlight off""" 339 | self._backlightval = Const.LCD_NOBACKLIGHT 340 | self._expander_write(value=0) 341 | 342 | def backlight(self) -> None: 343 | """Turn backlight on""" 344 | self._backlightval = Const.LCD_BACKLIGHT 345 | self._expander_write(value=0) 346 | 347 | def set_backlight(self, new_val: Union[int, bool]) -> None: 348 | """ 349 | Compatibility API functions for backlight 350 | 351 | :param new_val: The new backlight value 352 | :type new_val: Union[int, bool] 353 | """ 354 | if new_val: 355 | self.backlight() # turn backlight on 356 | else: 357 | self.no_backlight() # turn backlight off 358 | 359 | def get_backlight(self) -> bool: 360 | """ 361 | Get the backlight status 362 | 363 | :returns: The backlight status 364 | :rtype: bool 365 | """ 366 | return self._backlightval == Const.LCD_BACKLIGHT 367 | 368 | def autoscroll(self) -> None: 369 | """Set text 'right justified' from the cursor""" 370 | self._display_mode |= Const.LCD_ENTRYSHIFTINCREMENT 371 | self._command(value=(Const.LCD_ENTRYMODESET | self._display_mode)) 372 | 373 | def no_autoscroll(self) -> None: 374 | """Set text 'left justified' from the cursor""" 375 | self._display_mode &= ~Const.LCD_ENTRYSHIFTINCREMENT 376 | self._command(value=(Const.LCD_ENTRYMODESET | self._display_mode)) 377 | 378 | def create_char(self, location: int, charmap: List[int]) -> None: 379 | """ 380 | Fill the first 8 CGRAM locations with custom characters 381 | 382 | :param location: The location to store the custom character 383 | :type location: int 384 | :param charmap: The charmap aka custom character 385 | :type charmap: List[int] 386 | """ 387 | location &= 0x7 # we only have 8, locations 0-7 388 | 389 | self._command(value=(Const.LCD_SETCGRAMADDR | location << 3)) 390 | sleep_us(40) 391 | 392 | for x in range(0, 8): 393 | self._command(value=charmap[x], mode=Const.RS) 394 | sleep_us(40) 395 | 396 | def print(self, text: str) -> None: 397 | """ 398 | Print text on LCD 399 | 400 | :param test: Text to show on the LCD 401 | :type text: str 402 | """ 403 | _cursor_x, _cursor_y = self.cursor_position 404 | 405 | for char in text: 406 | self._command(value=ord(char), mode=Const.RS) 407 | 408 | self.cursor_position = (_cursor_x + len(text), _cursor_y) 409 | 410 | def _command(self, value: int, mode: int = 0) -> None: 411 | """ 412 | Send 8 bits command to I2C device 413 | 414 | :param value: The value 415 | :type value: int 416 | """ 417 | high_nib = value & 0xF0 418 | low_nib = (value << 4) & 0xF0 419 | self._write_4_bits(value=(high_nib | mode)) 420 | self._write_4_bits(value=(low_nib | mode)) 421 | 422 | def _write_4_bits(self, value: int) -> None: 423 | """ 424 | Write 4 bits to I2C device 425 | 426 | :param value: The value to send 427 | :type value: int 428 | """ 429 | self._expander_write(value=value) 430 | self._pulse_enable(value=value) 431 | 432 | def _pulse_enable(self, value: int) -> None: 433 | """ 434 | Pulse Enable (EN) pin 435 | 436 | :param value: The value to send 437 | :type value: int 438 | """ 439 | # Set Enable (EN) pin HIGH, pulse must be >450ns 440 | self._expander_write(value=(value | Const.EN)) 441 | sleep_us(1) 442 | 443 | # Set Enable (EN) pin LOW, needs >37us to settle 444 | self._expander_write(value=(value & ~Const.EN)) 445 | sleep_us(50) 446 | 447 | def _expander_write(self, value: int) -> None: 448 | """ 449 | Write data to I2C device (port expander) 450 | 451 | :param value: The value to send 452 | :type value: int 453 | """ 454 | self._i2c.writeto(self.addr, bytes([value | self._backlightval])) 455 | --------------------------------------------------------------------------------