From: Melissa LeBlanc-Williams Date: Tue, 26 May 2020 15:32:21 +0000 (-0700) Subject: Merge pull request #1 from makermelissa/master X-Git-Tag: 0.2.0~2 X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/commitdiff_plain/138d74a3457378ca0f684654f7cc4d6b8ce0301a?hp=d32dd241abaf4eafbb596b2b0590768c6bf75ef3 Merge pull request #1 from makermelissa/master Initial Commit --- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0ba1c3f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,51 @@ +name: Build CI + +on: [pull_request, push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Translate Repo Name For Build Tools filename_prefix + id: repo-name + run: | + echo ::set-output name=repo-name::Adafruit-Blinka-displayio + - name: Set up Python 3.6 + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Versions + run: | + python3 --version + - name: Checkout Current Repo + uses: actions/checkout@v1 + with: + submodules: true + - name: Checkout tools repo + uses: actions/checkout@v2 + with: + repository: adafruit/actions-ci-circuitpython-libs + path: actions-ci + - name: Install dependencies + # (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.) + run: | + source actions-ci/install.sh + - name: Pip install pylint, black, & Sphinx + run: | + pip install --force-reinstall pylint black==19.10b0 Sphinx sphinx-rtd-theme + - name: Library version + run: git describe --dirty --always --tags + - name: Check formatting + run: | + black --check --target-version=py35 . + - name: PyLint + run: | + pylint $( find . -path './*io/*.py' ) + pylint $( find . -path './*io.py' ) + - name: Build docs + working-directory: docs + run: sphinx-build -E -W -b html . _build/html diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..220e398 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release Actions + +on: + release: + types: [published] + +jobs: + upload-pypi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Check For setup.py + id: need-pypi + run: | + echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) + - name: Set up Python + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + env: + TWINE_USERNAME: ${{ secrets.pypi_username }} + TWINE_PASSWORD: ${{ secrets.pypi_password }} + run: | + python setup.py sdist + twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..966df0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.mpy +.idea +__pycache__ +_build +*.pyc +.env +.python-version +build*/ +bundles +*.DS_Store +.eggs +dist +**/*.egg-info +.vscode diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..d8f0ee8 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,433 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +# jobs=1 +jobs=2 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +# notes=FIXME,XXX,TODO +notes=FIXME,XXX + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules=board + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +# expected-line-ending-format= +expected-line-ending-format=LF + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +# class-name-hint=[A-Z_][a-zA-Z0-9]+$ +class-name-hint=[A-Z_][a-zA-Z0-9_]+$ + +# Regular expression matching correct class names +# class-rgx=[A-Z_][a-zA-Z0-9]+$ +class-rgx=[A-Z_][a-zA-Z0-9_]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +# good-names=i,j,k,ex,Run,_ +good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +# max-attributes=7 +max-attributes=11 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..f4243ad --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,3 @@ +python: + version: 3 +requirements_file: requirements.txt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a62e132 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ +# Adafruit Community Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and leaders pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level or type of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +We are committed to providing a friendly, safe and welcoming environment for +all. + +Examples of behavior that contributes to creating a positive environment +include: + +* Be kind and courteous to others +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Collaborating with other community members +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and sexual attention or advances +* The use of inappropriate images, including in a community member's avatar +* The use of inappropriate language, including in a community member's nickname +* Any spamming, flaming, baiting or other attention-stealing behavior +* Excessive or unwelcome helping; answering outside the scope of the question + asked +* Trolling, insulting/derogatory comments, and personal or political attacks +* Promoting or spreading disinformation, lies, or conspiracy theories against + a person, group, organisation, project, or community +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate + +The goal of the standards and moderation guidelines outlined here is to build +and maintain a respectful community. We ask that you don’t just aim to be +"technically unimpeachable", but rather try to be your best self. + +We value many things beyond technical expertise, including collaboration and +supporting others within our community. Providing a positive experience for +other community members can have a much more significant impact than simply +providing the correct answer. + +## Our Responsibilities + +Project leaders are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project leaders have the right and responsibility to remove, edit, or +reject messages, comments, commits, code, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any community member for other behaviors that they deem +inappropriate, threatening, offensive, or harmful. + +## Moderation + +Instances of behaviors that violate the Adafruit Community Code of Conduct +may be reported by any member of the community. Community members are +encouraged to report these situations, including situations they witness +involving other community members. + +You may report in the following ways: + +In any situation, you may send an email to . + +On the Adafruit Discord, you may send an open message from any channel +to all Community Moderators by tagging @community moderators. You may +also send an open message from any channel, or a direct message to +@kattni#1507, @tannewt#4653, @Dan Halbert#1614, @cater#2442, +@sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. + +Email and direct message reports will be kept confidential. + +In situations on Discord where the issue is particularly egregious, possibly +illegal, requires immediate action, or violates the Discord terms of service, +you should also report the message directly to Discord. + +These are the steps for upholding our community’s standards of conduct. + +1. Any member of the community may report any situation that violates the +Adafruit Community Code of Conduct. All reports will be reviewed and +investigated. +2. If the behavior is an egregious violation, the community member who +committed the violation may be banned immediately, without warning. +3. Otherwise, moderators will first respond to such behavior with a warning. +4. Moderators follow a soft "three strikes" policy - the community member may +be given another chance, if they are receptive to the warning and change their +behavior. +5. If the community member is unreceptive or unreasonable when warned by a +moderator, or the warning goes unheeded, they may be banned for a first or +second offense. Repeated offenses will result in the community member being +banned. + +## Scope + +This Code of Conduct and the enforcement policies listed above apply to all +Adafruit Community venues. This includes but is not limited to any community +spaces (both public and private), the entire Adafruit Discord server, and +Adafruit GitHub repositories. Examples of Adafruit Community spaces include +but are not limited to meet-ups, audio chats on the Adafruit Discord, or +interaction at a conference. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. As a community +member, you are representing our community, and are expected to behave +accordingly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +, +and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). + +For other projects adopting the Adafruit Community Code of +Conduct, please contact the maintainers of those projects for enforcement. +If you wish to use this code of conduct for your own project, consider +explicitly mentioning your moderation policy or making a copy with your +own moderation policy so as to avoid confusion. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa6d192 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index c154ebe..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Adafruit_Blinka_displayio -Displayio for Blinka diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..dbaa736 --- /dev/null +++ b/README.rst @@ -0,0 +1,64 @@ +Introduction +============ + +.. image:: https://readthedocs.org/projects/adafruit-blinka-blinka-displayio/badge/?version=latest + :target: https://circuitpython.readthedocs.io/projects/displayio/en/latest/ + :alt: Documentation Status + +.. image:: https://img.shields.io/discord/327254708534116352.svg + :target: https://discord.gg/nBQh6qu + :alt: Discord + +.. image:: https://github.com/adafruit/Adafruit_blinka_CircuitPython_displayio/workflows/Build%20CI/badge.svg + :target: https://github.com/adafruit/Adafruit_blinka_CircuitPython_displayio/actions + :alt: Build Status + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code Style: Black + +displayio for Blinka + + +Dependencies +============= +This driver depends on: + +* `Adafruit Blinka `_ + +Installing from PyPI +===================== + +On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from +PyPI `_. To install for current user: + +.. code-block:: shell + + pip3 install adafruit-blinka-displayio + +To install system-wide (this may be required in some cases): + +.. code-block:: shell + + sudo pip3 install adafruit-blinka-displayio + +To install in a virtual environment in your current project: + +.. code-block:: shell + + mkdir project-name && cd project-name + python3 -m venv .env + source .env/bin/activate + pip3 install adafruit-blinka-displayio + +Contributing +============ + +Contributions are welcome! Please read our `Code of Conduct +`_ +before contributing to help this project stay welcoming. + +Documentation +============= + +For information on building library documentation, please check out `this guide `_. diff --git a/displayio/__init__.py b/displayio/__init__.py new file mode 100644 index 0000000..c3b04ac --- /dev/null +++ b/displayio/__init__.py @@ -0,0 +1,64 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from displayio.bitmap import Bitmap +from displayio.colorconverter import ColorConverter +from displayio.display import Display +from displayio.epaperdisplay import EPaperDisplay +from displayio.fourwire import FourWire +from displayio.group import Group +from displayio.i2cdisplay import I2CDisplay +from displayio.ondiskbitmap import OnDiskBitmap +from displayio.palette import Palette +from displayio.parallelbus import ParallelBus +from displayio.shape import Shape +from displayio.tilegrid import TileGrid +from displayio.display import displays + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + + +def release_displays(): + """Releases any actively used displays so their busses and pins can be used again. + + Use this once in your code.py if you initialize a display. Place it right before the + initialization so the display is active as long as possible. + """ + for _disp in displays: + _disp._release() # pylint: disable=protected-access + displays.clear() diff --git a/displayio/bitmap.py b/displayio/bitmap.py new file mode 100644 index 0000000..1bc90aa --- /dev/null +++ b/displayio/bitmap.py @@ -0,0 +1,139 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.bitmap` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from recordclass import recordclass + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +Rectangle = recordclass("Rectangle", "x1 y1 x2 y2") + + +class Bitmap: + """Stores values of a certain size in a 2D array""" + + def __init__(self, width, height, value_count): + """Create a Bitmap object with the given fixed size. Each pixel stores a value that is + used to index into a corresponding palette. This enables differently colored sprites to + share the underlying Bitmap. value_count is used to minimize the memory used to store + the Bitmap. + """ + self._width = width + self._height = height + self._read_only = False + + if value_count < 0: + raise ValueError("value_count must be > 0") + + bits = 1 + while (value_count - 1) >> bits: + if bits < 8: + bits = bits << 1 + else: + bits += 8 + + self._bits_per_value = bits + + if ( + self._bits_per_value > 8 + and self._bits_per_value != 16 + and self._bits_per_value != 32 + ): + raise NotImplementedError("Invalid bits per value") + + self._data = (width * height) * [0] + self._dirty_area = Rectangle(0, 0, width, height) + + def __getitem__(self, index): + """ + Returns the value at the given index. The index can either be + an x,y tuple or an int equal to `y * width + x`. + """ + if isinstance(index, (tuple, list)): + index = (index[1] * self._width) + index[0] + if index >= len(self._data): + raise ValueError("Index {} is out of range".format(index)) + return self._data[index] + + def __setitem__(self, index, value): + """ + Sets the value at the given index. The index can either be + an x,y tuple or an int equal to `y * width + x`. + """ + if self._read_only: + raise RuntimeError("Read-only object") + if isinstance(index, (tuple, list)): + x = index[0] + y = index[1] + index = y * self._width + x + elif isinstance(index, int): + x = index % self._width + y = index // self._width + self._data[index] = value + if self._dirty_area.x1 == self._dirty_area.x2: + self._dirty_area.x1 = x + self._dirty_area.x2 = x + 1 + self._dirty_area.y1 = y + self._dirty_area.y2 = y + 1 + else: + if x < self._dirty_area.x1: + self._dirty_area.x1 = x + elif x >= self._dirty_area.x2: + self._dirty_area.x2 = x + 1 + if y < self._dirty_area.y1: + self._dirty_area.y1 = y + elif y >= self._dirty_area.y2: + self._dirty_area.y2 = y + 1 + + def _finish_refresh(self): + self._dirty_area.x1 = 0 + self._dirty_area.x2 = 0 + + def fill(self, value): + """Fills the bitmap with the supplied palette index value.""" + self._data = (self._width * self._height) * [value] + self._dirty_area = Rectangle(0, 0, self._width, self._height) + + @property + def width(self): + """Width of the bitmap. (read only)""" + return self._width + + @property + def height(self): + """Height of the bitmap. (read only)""" + return self._height diff --git a/displayio/colorconverter.py b/displayio/colorconverter.py new file mode 100644 index 0000000..ead3f34 --- /dev/null +++ b/displayio/colorconverter.py @@ -0,0 +1,124 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.colorconverter` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + + +class ColorConverter: + """Converts one color format to another. Color converter based on original displayio + code for consistency. + """ + + def __init__(self, *, dither=False): + """Create a ColorConverter object to convert color formats. + Only supports rgb888 to RGB565 currently. + :param bool dither: Adds random noise to dither the output image + """ + self._dither = dither + self._depth = 16 + + # pylint: disable=no-self-use + def _compute_rgb565(self, color): + self._depth = 16 + return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F + + def _compute_luma(self, color): + red = color >> 16 + green = (color >> 8) & 0xFF + blue = color & 0xFF + return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255 + + def _compute_chroma(self, color): + red = color >> 16 + green = (color >> 8) & 0xFF + blue = color & 0xFF + return max(red, green, blue) - min(red, green, blue) + + def _compute_hue(self, color): + red = color >> 16 + green = (color >> 8) & 0xFF + blue = color & 0xFF + max_color = max(red, green, blue) + chroma = self._compute_chroma(color) + if chroma == 0: + return 0 + hue = 0 + if max_color == red: + hue = (((green - blue) * 40) / chroma) % 240 + elif max_color == green: + hue = (((blue - red) + (2 * chroma)) * 40) / chroma + elif max_color == blue: + hue = (((red - green) + (4 * chroma)) * 40) / chroma + if hue < 0: + hue += 240 + + return hue + + def _dither_noise_1(self, noise): + noise = (noise >> 13) ^ noise + more_noise = ( + noise * (noise * noise * 60493 + 19990303) + 1376312589 + ) & 0x7FFFFFFF + return (more_noise / (1073741824.0 * 2)) * 255 + + def _dither_noise_2(self, x, y): + return self._dither_noise_1(x + y * 0xFFFF) + + def _compute_tricolor(self): + pass + + def convert(self, color): + "Converts the given rgb888 color to RGB565" + if self._dither: + return color # To Do: return a dithered color + return self._compute_rgb565(color) + + # pylint: enable=no-self-use + + @property + def dither(self): + """When true the color converter dithers the output by adding + random noise when truncating to display bitdepth + """ + return self._dither + + @dither.setter + def dither(self, value): + if not isinstance(value, bool): + raise ValueError("Value should be boolean") + self._dither = value diff --git a/displayio/display.py b/displayio/display.py new file mode 100644 index 0000000..4325671 --- /dev/null +++ b/displayio/display.py @@ -0,0 +1,324 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.display` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +import time +import struct +import threading +from PIL import Image +import numpy +from recordclass import recordclass + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +Rectangle = recordclass("Rectangle", "x1 y1 x2 y2") +displays = [] + +# pylint: disable=unnecessary-pass, unused-argument + +# pylint: disable=too-many-instance-attributes +class Display: + """This initializes a display and connects it into CircuitPython. Unlike other objects + in CircuitPython, Display objects live until ``displayio.release_displays()`` is called. + This is done so that CircuitPython can use the display itself. + + Most people should not use this class directly. Use a specific display driver instead + that will contain the initialization sequence at minimum. + + .. class:: + Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0, + color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1, + reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b, + write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None, + brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False, + auto_refresh=True, native_frames_per_second=60) + """ + + # pylint: disable=too-many-locals + def __init__( + self, + display_bus, + init_sequence, + *, + width, + height, + colstart=0, + rowstart=0, + rotation=0, + color_depth=16, + grayscale=False, + pixels_in_byte_share_row=True, + bytes_per_cell=1, + reverse_pixels_in_byte=False, + set_column_command=0x2A, + set_row_command=0x2B, + write_ram_command=0x2C, + set_vertical_scroll=0, + backlight_pin=None, + brightness_command=None, + brightness=1.0, + auto_brightness=False, + single_byte_bounds=False, + data_as_commands=False, + auto_refresh=True, + native_frames_per_second=60 + ): + """Create a Display object on the given display bus (`displayio.FourWire` or + `displayio.ParallelBus`). + + The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins + with a command byte followed by a byte to determine the parameter count and if a + delay is need after. When the top bit of the second byte is 1, the next byte will be + the delay time in milliseconds. The remaining 7 bits are the parameter count + excluding any delay byte. The third through final bytes are the remaining command + parameters. The next byte will begin a new command definition. Here is a portion of + ILI9341 init code: + .. code-block:: python + + init_sequence = ( + b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" + b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms) + b"\x29\x80\x78"# Display on then delay 0x78 (120ms) + ) + display = displayio.Display(display_bus, init_sequence, width=320, height=240) + + The first command is 0xe1 with 15 (0xf) parameters following. The second and third + are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters. + Multiple byte literals (b”“) are merged together on load. The parens are needed to + allow byte literals on subsequent lines. + + The initialization sequence should always leave the display memory access inline with + the scan of the display to minimize tearing artifacts. + """ + self._bus = display_bus + self._set_column_command = set_column_command + self._set_row_command = set_row_command + self._write_ram_command = write_ram_command + self._brightness_command = brightness_command + self._data_as_commands = data_as_commands + self._single_byte_bounds = single_byte_bounds + self._width = width + self._height = height + self._colstart = colstart + self._rowstart = rowstart + self._rotation = rotation + self._auto_brightness = auto_brightness + self._brightness = brightness + self._auto_refresh = auto_refresh + self._initialize(init_sequence) + self._buffer = Image.new("RGB", (width, height)) + self._subrectangles = [] + self._bounds_encoding = ">BB" if single_byte_bounds else ">HH" + self._current_group = None + displays.append(self) + self._refresh_thread = None + if self._auto_refresh: + self.auto_refresh = True + + # pylint: enable=too-many-locals + + def _initialize(self, init_sequence): + i = 0 + while i < len(init_sequence): + command = init_sequence[i] + data_size = init_sequence[i + 1] + delay = (data_size & 0x80) > 0 + data_size &= ~0x80 + self._write(command, init_sequence[i + 2 : i + 2 + data_size]) + delay_time_ms = 10 + if delay: + data_size += 1 + delay_time_ms = init_sequence[i + 1 + data_size] + if delay_time_ms == 255: + delay_time_ms = 500 + time.sleep(delay_time_ms / 1000) + i += 2 + data_size + + def _write(self, command, data): + if self._single_byte_bounds: + self._bus.send(True, bytes([command]) + data, toggle_every_byte=True) + else: + self._bus.send(True, bytes([command]), toggle_every_byte=True) + self._bus.send(False, data) + + def _release(self): + self._bus.release() + self._bus = None + + def show(self, group): + """Switches to displaying the given group of layers. When group is None, the + default CircuitPython terminal will be shown. + """ + self._current_group = group + + def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1): + """When auto refresh is off, waits for the target frame rate and then refreshes the + display, returning True. If the call has taken too long since the last refresh call + for the given target frame rate, then the refresh returns False immediately without + updating the screen to hopefully help getting caught up. + + If the time since the last successful refresh is below the minimum frame rate, then + an exception will be raised. Set minimum_frames_per_second to 0 to disable. + + When auto refresh is on, updates the display immediately. (The display will also + update without calls to this.) + """ + # Go through groups and and add each to buffer + if self._current_group is not None: + buffer = Image.new("RGBA", (self._width, self._height)) + # Recursively have everything draw to the image + self._current_group._fill_area(buffer) # pylint: disable=protected-access + # save image to buffer (or probably refresh buffer so we can compare) + self._buffer.paste(buffer) + time.sleep(1) + # Eventually calculate dirty rectangles here + self._subrectangles.append(Rectangle(0, 0, self._width, self._height)) + + for area in self._subrectangles: + self._refresh_display_area(area) + + def _refresh_loop(self): + while self._auto_refresh: + self.refresh() + + def _refresh_display_area(self, rectangle): + """Loop through dirty rectangles and redraw that area.""" + data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16") + color = ( + ((data[:, :, 0] & 0xF8) << 8) + | ((data[:, :, 1] & 0xFC) << 3) + | (data[:, :, 2] >> 3) + ) + + pixels = list( + numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist() + ) + + self._write( + self._set_column_command, + self._encode_pos( + rectangle.x1 + self._colstart, rectangle.x2 + self._colstart + ), + ) + self._write( + self._set_row_command, + self._encode_pos( + rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart + ), + ) + self._write(self._write_ram_command, pixels) + + def _encode_pos(self, x, y): + """Encode a postion into bytes.""" + return struct.pack(self._bounds_encoding, x, y) + + def fill_row(self, y, buffer): + """Extract the pixels from a single row""" + pass + + @property + def auto_refresh(self): + """True when the display is refreshed automatically.""" + return self._auto_refresh + + @auto_refresh.setter + def auto_refresh(self, value): + self._auto_refresh = value + if self._refresh_thread is None: + self._refresh_thread = threading.Thread( + target=self._refresh_loop, daemon=True + ) + if value and not self._refresh_thread.is_alive(): + # Start the thread + self._refresh_thread.start() + elif not value and self._refresh_thread.is_alive(): + # Stop the thread + self._refresh_thread.join() + + @property + def brightness(self): + """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`. + When `auto_brightness` is True, the value of `brightness` will change automatically. + If `brightness` is set, `auto_brightness` will be disabled and will be set to False. + """ + return self._brightness + + @brightness.setter + def brightness(self, value): + self._brightness = value + + @property + def auto_brightness(self): + """True when the display brightness is adjusted automatically, based on an ambient + light sensor or other method. Note that some displays may have this set to True by + default, but not actually implement automatic brightness adjustment. + `auto_brightness` is set to False if `brightness` is set manually. + """ + return self._auto_brightness + + @auto_brightness.setter + def auto_brightness(self, value): + self._auto_brightness = value + + @property + def width(self): + """Display Width""" + return self._width + + @property + def height(self): + """Display Height""" + return self._height + + @property + def rotation(self): + """The rotation of the display as an int in degrees.""" + return self._rotation + + @rotation.setter + def rotation(self, value): + if value not in (0, 90, 180, 270): + raise ValueError("Rotation must be 0/90/180/270") + self._rotation = value + + @property + def bus(self): + """Current Display Bus""" + return self._bus + + +# pylint: enable=too-many-instance-attributes diff --git a/displayio/epaperdisplay.py b/displayio/epaperdisplay.py new file mode 100644 index 0000000..a0946c2 --- /dev/null +++ b/displayio/epaperdisplay.py @@ -0,0 +1,880 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.epaperdisplay` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +import time +import digitalio +from recordclass import recordclass +from PIL import Image +from displayio.bitmap import Bitmap +from displayio.colorconverter import ColorConverter + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +# pylint: disable=unnecessary-pass, unused-argument + +Rectangle = recordclass("Rectangle", "x1 y1 x2 y2") +Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y") + + +class EPaperDisplay: + """Manage updating an epaper display over a display bus + + This initializes an epaper display and connects it into CircuitPython. Unlike other + objects in CircuitPython, EPaperDisplay objects live until + displayio.release_displays() is called. This is done so that CircuitPython can use + the display itself. + + Most people should not use this class directly. Use a specific display driver instead + that will contain the startup and shutdown sequences at minimum. + """ + + # pylint: disable=too-many-locals + def __init__( + self, + display_bus, + start_sequence, + stop_sequence, + *, + width, + height, + ram_width, + ram_height, + colstart=0, + rowstart=0, + rotation=0, + set_column_window_command=None, + set_row_window_command=None, + single_byte_bounds=False, + write_black_ram_command, + black_bits_inverted=False, + write_color_ram_command=None, + color_bits_inverted=False, + highlight_color=0x000000, + refresh_display_command, + refresh_time=40, + busy_pin=None, + busy_state=True, + seconds_per_frame=180, + always_toggle_chip_select=False + ): + """ + Create a EPaperDisplay object on the given display bus (displayio.FourWire or + displayio.ParallelBus). + + The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every + command begins with a command byte followed by a byte to determine the parameter + count and if a delay is need after. When the top bit of the second byte is 1, the + next byte will be the delay time in milliseconds. The remaining 7 bits are the + parameter count excluding any delay byte. The third through final bytes are the + remaining command parameters. The next byte will begin a new command definition. + """ + pass + + # pylint: enable=too-many-locals + + def show(self, group): + """Switches to displaying the given group of layers. When group is None, the default + CircuitPython terminal will be shown (eventually). + """ + pass + + def refresh(self): + """Refreshes the display immediately or raises an exception if too soon. Use + ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur. + """ + pass + + @property + def time_to_refresh(self): + """Time, in fractional seconds, until the ePaper display can be refreshed.""" + return 0 + + @property + def width(self): + """Display Width""" + pass + + @property + def height(self): + """Display Height""" + pass + + @property + def bus(self): + """Current Display Bus""" + pass + + +class FourWire: + """Manage updating a display over SPI four wire protocol in the background while + Python code runs. It doesn’t handle display initialization. + """ + + def __init__( + self, + spi_bus, + *, + command, + chip_select, + reset=None, + baudrate=24000000, + polarity=0, + phase=0 + ): + """Create a FourWire object associated with the given pins. + + The SPI bus and pins are then in use by the display until + displayio.release_displays() is called even after a reload. (It does this so + CircuitPython can use the display after your code is done.) + So, the first time you initialize a display bus in code.py you should call + :py:func`displayio.release_displays` first, otherwise it will error after the + first code.py run. + """ + self._dc = digitalio.DigitalInOut(command) + self._dc.switch_to_output() + self._chip_select = digitalio.DigitalInOut(chip_select) + self._chip_select.switch_to_output(value=True) + + if reset is not None: + self._reset = digitalio.DigitalInOut(reset) + self._reset.switch_to_output(value=True) + else: + self._reset = None + self._spi = spi_bus + while self._spi.try_lock(): + pass + self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase) + self._spi.unlock() + + def _release(self): + self.reset() + self._spi.deinit() + self._dc.deinit() + self._chip_select.deinit() + if self._reset is not None: + self._reset.deinit() + + def reset(self): + """Performs a hardware reset via the reset pin. + Raises an exception if called when no reset pin is available. + """ + if self._reset is not None: + self._reset.value = False + time.sleep(0.001) + self._reset.value = True + time.sleep(0.001) + else: + raise RuntimeError("No reset pin defined") + + def send(self, is_command, data, *, toggle_every_byte=False): + """Sends the given command value followed by the full set of data. Display state, + such as vertical scroll, set via ``send`` may or may not be reset once the code is + done. + """ + while self._spi.try_lock(): + pass + self._dc.value = not is_command + if toggle_every_byte: + for byte in data: + self._spi.write(bytes([byte])) + self._chip_select.value = True + time.sleep(0.000001) + self._chip_select.value = False + else: + self._spi.write(data) + self._spi.unlock() + + +class Group: + """Manage a group of sprites and groups and how they are inter-related.""" + + def __init__(self, *, max_size=4, scale=1, x=0, y=0): + """Create a Group of a given size and scale. Scale is in + one dimension. For example, scale=2 leads to a layer’s + pixel being 2x2 pixels when in the group. + """ + if not isinstance(max_size, int) or max_size < 1: + raise ValueError("Max Size must be >= 1") + self._max_size = max_size + if not isinstance(scale, int) or scale < 1: + raise ValueError("Scale must be >= 1") + self._scale = scale + self._x = x + self._y = y + self._hidden = False + self._layers = [] + self._supported_types = (TileGrid, Group) + self._absolute_transform = None + self.in_group = False + self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False) + + def update_transform(self, parent_transform): + """Update the parent transform and child transforms""" + self.in_group = parent_transform is not None + if self.in_group: + x = self._x + y = self._y + if parent_transform.transpose_xy: + x, y = y, x + self._absolute_transform.x = parent_transform.x + parent_transform.dx * x + self._absolute_transform.y = parent_transform.y + parent_transform.dy * y + self._absolute_transform.dx = parent_transform.dx * self._scale + self._absolute_transform.dy = parent_transform.dy * self._scale + self._absolute_transform.transpose_xy = parent_transform.transpose_xy + self._absolute_transform.mirror_x = parent_transform.mirror_x + self._absolute_transform.mirror_y = parent_transform.mirror_y + self._absolute_transform.scale = parent_transform.scale * self._scale + self._update_child_transforms() + + def _update_child_transforms(self): + if self.in_group: + for layer in self._layers: + layer.update_transform(self._absolute_transform) + + def _removal_cleanup(self, index): + layer = self._layers[index] + layer.update_transform(None) + + def _layer_update(self, index): + layer = self._layers[index] + layer.update_transform(self._absolute_transform) + + def append(self, layer): + """Append a layer to the group. It will be drawn + above other layers. + """ + self.insert(len(self._layers), layer) + + def insert(self, index, layer): + """Insert a layer into the group.""" + if not isinstance(layer, self._supported_types): + raise ValueError("Invalid Group Member") + if layer.in_group: + raise ValueError("Layer already in a group.") + if len(self._layers) == self._max_size: + raise RuntimeError("Group full") + self._layers.insert(index, layer) + self._layer_update(index) + + def index(self, layer): + """Returns the index of the first copy of layer. + Raises ValueError if not found. + """ + return self._layers.index(layer) + + def pop(self, index=-1): + """Remove the ith item and return it.""" + self._removal_cleanup(index) + return self._layers.pop(index) + + def remove(self, layer): + """Remove the first copy of layer. Raises ValueError + if it is not present.""" + index = self.index(layer) + self._layers.pop(index) + + def __len__(self): + """Returns the number of layers in a Group""" + return len(self._layers) + + def __getitem__(self, index): + """Returns the value at the given index.""" + return self._layers[index] + + def __setitem__(self, index, value): + """Sets the value at the given index.""" + self._removal_cleanup(index) + self._layers[index] = value + self._layer_update(index) + + def __delitem__(self, index): + """Deletes the value at the given index.""" + del self._layers[index] + + def _fill_area(self, buffer): + if self._hidden: + return + + for layer in self._layers: + if isinstance(layer, (Group, TileGrid)): + layer._fill_area(buffer) # pylint: disable=protected-access + + @property + def hidden(self): + """True when the Group and all of it’s layers are not visible. When False, the + Group’s layers are visible if they haven’t been hidden. + """ + return self._hidden + + @hidden.setter + def hidden(self, value): + if not isinstance(value, (bool, int)): + raise ValueError("Expecting a boolean or integer value") + self._hidden = bool(value) + + @property + def scale(self): + """Scales each pixel within the Group in both directions. For example, when + scale=2 each pixel will be represented by 2x2 pixels. + """ + return self._scale + + @scale.setter + def scale(self, value): + if not isinstance(value, int) or value < 1: + raise ValueError("Scale must be >= 1") + if self._scale != value: + parent_scale = self._absolute_transform.scale / self._scale + self._absolute_transform.dx = ( + self._absolute_transform.dx / self._scale * value + ) + self._absolute_transform.dy = ( + self._absolute_transform.dy / self._scale * value + ) + self._absolute_transform.scale = parent_scale * value + + self._scale = value + self._update_child_transforms() + + @property + def x(self): + """X position of the Group in the parent.""" + return self._x + + @x.setter + def x(self, value): + if not isinstance(value, int): + raise ValueError("x must be an integer") + if self._x != value: + if self._absolute_transform.transpose_xy: + dy_value = self._absolute_transform.dy / self._scale + self._absolute_transform.y += dy_value * (value - self._x) + else: + dx_value = self._absolute_transform.dx / self._scale + self._absolute_transform.x += dx_value * (value - self._x) + self._x = value + self._update_child_transforms() + + @property + def y(self): + """Y position of the Group in the parent.""" + return self._y + + @y.setter + def y(self, value): + if not isinstance(value, int): + raise ValueError("y must be an integer") + if self._y != value: + if self._absolute_transform.transpose_xy: + dx_value = self._absolute_transform.dx / self._scale + self._absolute_transform.x += dx_value * (value - self._y) + else: + dy_value = self._absolute_transform.dy / self._scale + self._absolute_transform.y += dy_value * (value - self._y) + self._y = value + self._update_child_transforms() + + +class I2CDisplay: + """Manage updating a display over I2C in the background while Python code runs. + It doesn’t handle display initialization. + """ + + def __init__(self, i2c_bus, *, device_address, reset=None): + """Create a I2CDisplay object associated with the given I2C bus and reset pin. + + The I2C bus and pins are then in use by the display until displayio.release_displays() is + called even after a reload. (It does this so CircuitPython can use the display after your + code is done.) So, the first time you initialize a display bus in code.py you should call + :py:func`displayio.release_displays` first, otherwise it will error after the first + code.py run. + """ + pass + + def reset(self): + """Performs a hardware reset via the reset pin. Raises an exception if called + when no reset pin is available. + """ + pass + + def send(self, command, data): + """Sends the given command value followed by the full set of data. Display state, + such as vertical scroll, set via send may or may not be reset once the code is + done. + """ + pass + + +class OnDiskBitmap: + """ + Loads values straight from disk. This minimizes memory use but can lead to much slower + pixel load times. These load times may result in frame tearing where only part of the + image is visible.""" + + def __init__(self, file): + self._image = Image.open(file) + + @property + def width(self): + """Width of the bitmap. (read only)""" + return self._image.width + + @property + def height(self): + """Height of the bitmap. (read only)""" + return self._image.height + + +class Palette: + """Map a pixel palette_index to a full color. Colors are transformed to the display’s + format internally to save memory. + """ + + def __init__(self, color_count): + """Create a Palette object to store a set number of colors.""" + self._needs_refresh = False + + self._colors = [] + for _ in range(color_count): + self._colors.append(self._make_color(0)) + + def _make_color(self, value, transparent=False): + color = { + "transparent": transparent, + "rgb888": 0, + } + if isinstance(value, (tuple, list, bytes, bytearray)): + value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF + elif isinstance(value, int): + if not 0 <= value <= 0xFFFFFF: + raise ValueError("Color must be between 0x000000 and 0xFFFFFF") + else: + raise TypeError("Color buffer must be a buffer, tuple, list, or int") + color["rgb888"] = value + self._needs_refresh = True + + return color + + def __len__(self): + """Returns the number of colors in a Palette""" + return len(self._colors) + + def __setitem__(self, index, value): + """Sets the pixel color at the given index. The index should be + an integer in the range 0 to color_count-1. + + The value argument represents a color, and can be from 0x000000 to 0xFFFFFF + (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or + 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers. + """ + if self._colors[index]["rgb888"] != value: + self._colors[index] = self._make_color(value) + + def __getitem__(self, index): + if not 0 <= index < len(self._colors): + raise ValueError("Palette index out of range") + return self._colors[index] + + def make_transparent(self, palette_index): + """Set the palette index to be a transparent color""" + self._colors[palette_index]["transparent"] = True + + def make_opaque(self, palette_index): + """Set the palette index to be an opaque color""" + self._colors[palette_index]["transparent"] = False + + +class ParallelBus: + """Manage updating a display over 8-bit parallel bus in the background while Python code + runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets. + It doesn’t handle display initialization. + """ + + def __init__(self, i2c_bus, *, device_address, reset=None): + """Create a ParallelBus object associated with the given pins. The + bus is inferred from data0 by implying the next 7 additional pins on a given GPIO + port. + + The parallel bus and pins are then in use by the display until + displayio.release_displays() is called even after a reload. (It does this so + CircuitPython can use the display after your code is done.) So, the first time you + initialize a display bus in code.py you should call + :py:func`displayio.release_displays` first, otherwise it will error after the first + code.py run. + """ + pass + + def reset(self): + """Performs a hardware reset via the reset pin. Raises an exception if called when + no reset pin is available. + """ + pass + + def send(self, command, data): + """Sends the given command value followed by the full set of data. Display state, + such as vertical scroll, set via ``send`` may or may not be reset once the code is + done. + """ + pass + + +class Shape(Bitmap): + """Create a Shape object with the given fixed size. Each pixel is one bit and is stored + by the column boundaries of the shape on each row. Each row’s boundary defaults to the + full row. + """ + + def __init__(self, width, height, *, mirror_x=False, mirror_y=False): + """Create a Shape object with the given fixed size. Each pixel is one bit and is + stored by the column boundaries of the shape on each row. Each row’s boundary + defaults to the full row. + """ + super().__init__(width, height, 2) + + def set_boundary(self, y, start_x, end_x): + """Loads pre-packed data into the given row.""" + pass + + +# pylint: disable=too-many-instance-attributes +class TileGrid: + """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple + grids can share bitmaps and pixel shaders. + + A single tile grid is also known as a Sprite. + """ + + def __init__( + self, + bitmap, + *, + pixel_shader, + width=1, + height=1, + tile_width=None, + tile_height=None, + default_tile=0, + x=0, + y=0 + ): + """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is + used to convert the value and its location to a display native pixel color. This may + be a simple color palette lookup, a gradient, a pattern or a color transformer. + + tile_width and tile_height match the height of the bitmap by default. + """ + if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)): + raise ValueError("Unsupported Bitmap type") + self._bitmap = bitmap + bitmap_width = bitmap.width + bitmap_height = bitmap.height + + if not isinstance(pixel_shader, (ColorConverter, Palette)): + raise ValueError("Unsupported Pixel Shader type") + self._pixel_shader = pixel_shader + self._hidden = False + self._x = x + self._y = y + self._width = width # Number of Tiles Wide + self._height = height # Number of Tiles High + self._transpose_xy = False + self._flip_x = False + self._flip_y = False + if tile_width is None: + tile_width = bitmap_width + if tile_height is None: + tile_height = bitmap_height + if bitmap_width % tile_width != 0: + raise ValueError("Tile width must exactly divide bitmap width") + self._tile_width = tile_width + if bitmap_height % tile_height != 0: + raise ValueError("Tile height must exactly divide bitmap height") + self._tile_height = tile_height + if not 0 <= default_tile <= 255: + raise ValueError("Default Tile is out of range") + self._pixel_width = width * tile_width + self._pixel_height = height * tile_height + self._tiles = (self._width * self._height) * [default_tile] + self.in_group = False + self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False) + self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height) + self._moved = False + + def update_transform(self, absolute_transform): + """Update the parent transform and child transforms""" + self._absolute_transform = absolute_transform + if self._absolute_transform is not None: + self._update_current_x() + self._update_current_y() + + def _update_current_x(self): + if self._transpose_xy: + width = self._pixel_height + else: + width = self._pixel_width + if self._absolute_transform.transpose_xy: + self._current_area.y1 = ( + self._absolute_transform.y + self._absolute_transform.dy * self._x + ) + self._current_area.y2 = ( + self._absolute_transform.y + + self._absolute_transform.dy * (self._x + width) + ) + if self._current_area.y2 < self._current_area.y1: + self._current_area.y1, self._current_area.y2 = ( + self._current_area.y2, + self._current_area.y1, + ) + else: + self._current_area.x1 = ( + self._absolute_transform.x + self._absolute_transform.dx * self._x + ) + self._current_area.x2 = ( + self._absolute_transform.x + + self._absolute_transform.dx * (self._x + width) + ) + if self._current_area.x2 < self._current_area.x1: + self._current_area.x1, self._current_area.x2 = ( + self._current_area.x2, + self._current_area.x1, + ) + + def _update_current_y(self): + if self._transpose_xy: + height = self._pixel_width + else: + height = self._pixel_height + if self._absolute_transform.transpose_xy: + self._current_area.x1 = ( + self._absolute_transform.x + self._absolute_transform.dx * self._y + ) + self._current_area.x2 = ( + self._absolute_transform.x + + self._absolute_transform.dx * (self._y + height) + ) + if self._current_area.x2 < self._current_area.x1: + self._current_area.x1, self._current_area.x2 = ( + self._current_area.x2, + self._current_area.x1, + ) + else: + self._current_area.y1 = ( + self._absolute_transform.y + self._absolute_transform.dy * self._y + ) + self._current_area.y2 = ( + self._absolute_transform.y + + self._absolute_transform.dy * (self._y + height) + ) + if self._current_area.y2 < self._current_area.y1: + self._current_area.y1, self._current_area.y2 = ( + self._current_area.y2, + self._current_area.y1, + ) + + # pylint: disable=too-many-locals + def _fill_area(self, buffer): + """Draw onto the image""" + if self._hidden: + return + + image = Image.new( + "RGBA", + (self._width * self._tile_width, self._height * self._tile_height), + (0, 0, 0, 0), + ) + + tile_count_x = self._bitmap.width // self._tile_width + x = self._x + y = self._y + + for tile_x in range(0, self._width): + for tile_y in range(0, self._height): + tile_index = self._tiles[tile_y * self._width + tile_x] + tile_index_x = tile_index % tile_count_x + tile_index_y = tile_index // tile_count_x + for pixel_x in range(self._tile_width): + for pixel_y in range(self._tile_height): + image_x = tile_x * self._tile_width + pixel_x + image_y = tile_y * self._tile_height + pixel_y + bitmap_x = tile_index_x * self._tile_width + pixel_x + bitmap_y = tile_index_y * self._tile_height + pixel_y + pixel_color = self._pixel_shader[ + self._bitmap[bitmap_x, bitmap_y] + ] + if not pixel_color["transparent"]: + image.putpixel((image_x, image_y), pixel_color["rgb888"]) + if self._absolute_transform is not None: + if self._absolute_transform.scale > 1: + image = image.resize( + ( + self._pixel_width * self._absolute_transform.scale, + self._pixel_height * self._absolute_transform.scale, + ), + resample=Image.NEAREST, + ) + if self._absolute_transform.mirror_x: + image = image.transpose(Image.FLIP_LEFT_RIGHT) + if self._absolute_transform.mirror_y: + image = image.transpose(Image.FLIP_TOP_BOTTOM) + if self._absolute_transform.transpose_xy: + image = image.transpose(Image.TRANSPOSE) + x *= self._absolute_transform.dx + y *= self._absolute_transform.dy + x += self._absolute_transform.x + y += self._absolute_transform.y + buffer.alpha_composite(image, (x, y)) + + # pylint: enable=too-many-locals + + @property + def hidden(self): + """True when the TileGrid is hidden. This may be False even + when a part of a hidden Group.""" + return self._hidden + + @hidden.setter + def hidden(self, value): + if not isinstance(value, (bool, int)): + raise ValueError("Expecting a boolean or integer value") + self._hidden = bool(value) + + @property + def x(self): + """X position of the left edge in the parent.""" + return self._x + + @x.setter + def x(self, value): + if not isinstance(value, int): + raise TypeError("X should be a integer type") + if self._x != value: + self._x = value + self._update_current_x() + + @property + def y(self): + """Y position of the top edge in the parent.""" + return self._y + + @y.setter + def y(self, value): + if not isinstance(value, int): + raise TypeError("Y should be a integer type") + if self._y != value: + self._y = value + self._update_current_y() + + @property + def flip_x(self): + """If true, the left edge rendered will be the right edge of the right-most tile.""" + return self._flip_x + + @flip_x.setter + def flip_x(self, value): + if not isinstance(value, bool): + raise TypeError("Flip X should be a boolean type") + if self._flip_x != value: + self._flip_x = value + + @property + def flip_y(self): + """If true, the top edge rendered will be the bottom edge of the bottom-most tile.""" + return self._flip_y + + @flip_y.setter + def flip_y(self, value): + if not isinstance(value, bool): + raise TypeError("Flip Y should be a boolean type") + if self._flip_y != value: + self._flip_y = value + + @property + def transpose_xy(self): + """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90 + degree rotation can be achieved along with the corresponding mirrored version. + """ + return self._transpose_xy + + @transpose_xy.setter + def transpose_xy(self, value): + if not isinstance(value, bool): + raise TypeError("Transpose XY should be a boolean type") + if self._transpose_xy != value: + self._transpose_xy = value + self._update_current_x() + self._update_current_y() + + @property + def pixel_shader(self): + """The pixel shader of the tilegrid.""" + pass + + def __getitem__(self, index): + """Returns the tile index at the given index. The index can either be + an x,y tuple or an int equal to ``y * width + x``'. + """ + if isinstance(index, (tuple, list)): + x = index[0] + y = index[1] + index = y * self._width + x + elif isinstance(index, int): + x = index % self._width + y = index // self._width + if x > self._width or y > self._height or index >= len(self._tiles): + raise ValueError("Tile index out of bounds") + return self._tiles[index] + + def __setitem__(self, index, value): + """Sets the tile index at the given index. The index can either be + an x,y tuple or an int equal to ``y * width + x``. + """ + if isinstance(index, (tuple, list)): + x = index[0] + y = index[1] + index = y * self._width + x + elif isinstance(index, int): + x = index % self._width + y = index // self._width + if x > self._width or y > self._height or index >= len(self._tiles): + raise ValueError("Tile index out of bounds") + if not 0 <= value <= 255: + raise ValueError("Tile value out of bounds") + self._tiles[index] = value + + +# pylint: enable=too-many-instance-attributes diff --git a/displayio/fourwire.py b/displayio/fourwire.py new file mode 100644 index 0000000..5d8a433 --- /dev/null +++ b/displayio/fourwire.py @@ -0,0 +1,122 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.fourwire` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +import time +import digitalio + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + + +class FourWire: + """Manage updating a display over SPI four wire protocol in the background while + Python code runs. It doesn’t handle display initialization. + """ + + def __init__( + self, + spi_bus, + *, + command, + chip_select, + reset=None, + baudrate=24000000, + polarity=0, + phase=0 + ): + """Create a FourWire object associated with the given pins. + + The SPI bus and pins are then in use by the display until + displayio.release_displays() is called even after a reload. (It does this so + CircuitPython can use the display after your code is done.) + So, the first time you initialize a display bus in code.py you should call + :py:func`displayio.release_displays` first, otherwise it will error after the + first code.py run. + """ + self._dc = digitalio.DigitalInOut(command) + self._dc.switch_to_output() + self._chip_select = digitalio.DigitalInOut(chip_select) + self._chip_select.switch_to_output(value=True) + + if reset is not None: + self._reset = digitalio.DigitalInOut(reset) + self._reset.switch_to_output(value=True) + else: + self._reset = None + self._spi = spi_bus + while self._spi.try_lock(): + pass + self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase) + self._spi.unlock() + + def _release(self): + self.reset() + self._spi.deinit() + self._dc.deinit() + self._chip_select.deinit() + if self._reset is not None: + self._reset.deinit() + + def reset(self): + """Performs a hardware reset via the reset pin. + Raises an exception if called when no reset pin is available. + """ + if self._reset is not None: + self._reset.value = False + time.sleep(0.001) + self._reset.value = True + time.sleep(0.001) + else: + raise RuntimeError("No reset pin defined") + + def send(self, is_command, data, *, toggle_every_byte=False): + """Sends the given command value followed by the full set of data. Display state, + such as vertical scroll, set via ``send`` may or may not be reset once the code is + done. + """ + while self._spi.try_lock(): + pass + self._dc.value = not is_command + if toggle_every_byte: + for byte in data: + self._spi.write(bytes([byte])) + self._chip_select.value = True + time.sleep(0.000001) + self._chip_select.value = False + else: + self._spi.write(data) + self._spi.unlock() diff --git a/displayio/group.py b/displayio/group.py new file mode 100644 index 0000000..d28d83b --- /dev/null +++ b/displayio/group.py @@ -0,0 +1,235 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.group` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from recordclass import recordclass +from displayio.tilegrid import TileGrid + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + + +Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y") + + +class Group: + """Manage a group of sprites and groups and how they are inter-related.""" + + def __init__(self, *, max_size=4, scale=1, x=0, y=0): + """Create a Group of a given size and scale. Scale is in + one dimension. For example, scale=2 leads to a layer’s + pixel being 2x2 pixels when in the group. + """ + if not isinstance(max_size, int) or max_size < 1: + raise ValueError("Max Size must be >= 1") + self._max_size = max_size + if not isinstance(scale, int) or scale < 1: + raise ValueError("Scale must be >= 1") + self._scale = scale + self._x = x + self._y = y + self._hidden = False + self._layers = [] + self._supported_types = (TileGrid, Group) + self._absolute_transform = None + self.in_group = False + self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False) + + def update_transform(self, parent_transform): + """Update the parent transform and child transforms""" + self.in_group = parent_transform is not None + if self.in_group: + x = self._x + y = self._y + if parent_transform.transpose_xy: + x, y = y, x + self._absolute_transform.x = parent_transform.x + parent_transform.dx * x + self._absolute_transform.y = parent_transform.y + parent_transform.dy * y + self._absolute_transform.dx = parent_transform.dx * self._scale + self._absolute_transform.dy = parent_transform.dy * self._scale + self._absolute_transform.transpose_xy = parent_transform.transpose_xy + self._absolute_transform.mirror_x = parent_transform.mirror_x + self._absolute_transform.mirror_y = parent_transform.mirror_y + self._absolute_transform.scale = parent_transform.scale * self._scale + self._update_child_transforms() + + def _update_child_transforms(self): + if self.in_group: + for layer in self._layers: + layer.update_transform(self._absolute_transform) + + def _removal_cleanup(self, index): + layer = self._layers[index] + layer.update_transform(None) + + def _layer_update(self, index): + layer = self._layers[index] + layer.update_transform(self._absolute_transform) + + def append(self, layer): + """Append a layer to the group. It will be drawn + above other layers. + """ + self.insert(len(self._layers), layer) + + def insert(self, index, layer): + """Insert a layer into the group.""" + if not isinstance(layer, self._supported_types): + raise ValueError("Invalid Group Member") + if layer.in_group: + raise ValueError("Layer already in a group.") + if len(self._layers) == self._max_size: + raise RuntimeError("Group full") + self._layers.insert(index, layer) + self._layer_update(index) + + def index(self, layer): + """Returns the index of the first copy of layer. + Raises ValueError if not found. + """ + return self._layers.index(layer) + + def pop(self, index=-1): + """Remove the ith item and return it.""" + self._removal_cleanup(index) + return self._layers.pop(index) + + def remove(self, layer): + """Remove the first copy of layer. Raises ValueError + if it is not present.""" + index = self.index(layer) + self._layers.pop(index) + + def __len__(self): + """Returns the number of layers in a Group""" + return len(self._layers) + + def __getitem__(self, index): + """Returns the value at the given index.""" + return self._layers[index] + + def __setitem__(self, index, value): + """Sets the value at the given index.""" + self._removal_cleanup(index) + self._layers[index] = value + self._layer_update(index) + + def __delitem__(self, index): + """Deletes the value at the given index.""" + del self._layers[index] + + def _fill_area(self, buffer): + if self._hidden: + return + + for layer in self._layers: + if isinstance(layer, (Group, TileGrid)): + layer._fill_area(buffer) # pylint: disable=protected-access + + @property + def hidden(self): + """True when the Group and all of it’s layers are not visible. When False, the + Group’s layers are visible if they haven’t been hidden. + """ + return self._hidden + + @hidden.setter + def hidden(self, value): + if not isinstance(value, (bool, int)): + raise ValueError("Expecting a boolean or integer value") + self._hidden = bool(value) + + @property + def scale(self): + """Scales each pixel within the Group in both directions. For example, when + scale=2 each pixel will be represented by 2x2 pixels. + """ + return self._scale + + @scale.setter + def scale(self, value): + if not isinstance(value, int) or value < 1: + raise ValueError("Scale must be >= 1") + if self._scale != value: + parent_scale = self._absolute_transform.scale / self._scale + self._absolute_transform.dx = ( + self._absolute_transform.dx / self._scale * value + ) + self._absolute_transform.dy = ( + self._absolute_transform.dy / self._scale * value + ) + self._absolute_transform.scale = parent_scale * value + + self._scale = value + self._update_child_transforms() + + @property + def x(self): + """X position of the Group in the parent.""" + return self._x + + @x.setter + def x(self, value): + if not isinstance(value, int): + raise ValueError("x must be an integer") + if self._x != value: + if self._absolute_transform.transpose_xy: + dy_value = self._absolute_transform.dy / self._scale + self._absolute_transform.y += dy_value * (value - self._x) + else: + dx_value = self._absolute_transform.dx / self._scale + self._absolute_transform.x += dx_value * (value - self._x) + self._x = value + self._update_child_transforms() + + @property + def y(self): + """Y position of the Group in the parent.""" + return self._y + + @y.setter + def y(self, value): + if not isinstance(value, int): + raise ValueError("y must be an integer") + if self._y != value: + if self._absolute_transform.transpose_xy: + dx_value = self._absolute_transform.dx / self._scale + self._absolute_transform.x += dx_value * (value - self._y) + else: + dy_value = self._absolute_transform.dy / self._scale + self._absolute_transform.y += dy_value * (value - self._y) + self._y = value + self._update_child_transforms() diff --git a/displayio/i2cdisplay.py b/displayio/i2cdisplay.py new file mode 100644 index 0000000..21fa371 --- /dev/null +++ b/displayio/i2cdisplay.py @@ -0,0 +1,71 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.i2cdisplay` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +# pylint: disable=unnecessary-pass, unused-argument + + +class I2CDisplay: + """Manage updating a display over I2C in the background while Python code runs. + It doesn’t handle display initialization. + """ + + def __init__(self, i2c_bus, *, device_address, reset=None): + """Create a I2CDisplay object associated with the given I2C bus and reset pin. + + The I2C bus and pins are then in use by the display until displayio.release_displays() is + called even after a reload. (It does this so CircuitPython can use the display after your + code is done.) So, the first time you initialize a display bus in code.py you should call + :py:func`displayio.release_displays` first, otherwise it will error after the first + code.py run. + """ + pass + + def reset(self): + """Performs a hardware reset via the reset pin. Raises an exception if called + when no reset pin is available. + """ + pass + + def send(self, command, data): + """Sends the given command value followed by the full set of data. Display state, + such as vertical scroll, set via send may or may not be reset once the code is + done. + """ + pass diff --git a/displayio/ondiskbitmap.py b/displayio/ondiskbitmap.py new file mode 100644 index 0000000..3a08ab3 --- /dev/null +++ b/displayio/ondiskbitmap.py @@ -0,0 +1,63 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.ondiskbitmap` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from PIL import Image + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +# pylint: disable=unnecessary-pass, unused-argument + + +class OnDiskBitmap: + """ + Loads values straight from disk. This minimizes memory use but can lead to much slower + pixel load times. These load times may result in frame tearing where only part of the + image is visible.""" + + def __init__(self, file): + self._image = Image.open(file) + + @property + def width(self): + """Width of the bitmap. (read only)""" + return self._image.width + + @property + def height(self): + """Height of the bitmap. (read only)""" + return self._image.height diff --git a/displayio/palette.py b/displayio/palette.py new file mode 100644 index 0000000..1812dbf --- /dev/null +++ b/displayio/palette.py @@ -0,0 +1,113 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.palette` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + + +class Palette: + """Map a pixel palette_index to a full color. Colors are transformed to the display’s + format internally to save memory. + """ + + def __init__(self, color_count): + """Create a Palette object to store a set number of colors.""" + self._needs_refresh = False + + self._colors = [] + for _ in range(color_count): + self._colors.append(self._make_color(0)) + self._update_rgba(len(self._colors) - 1) + + def _update_rgba(self, index): + color = self._colors[index]["rgb888"] + transparent = self._colors[index]["transparent"] + self._colors[index]["rgba"] = ( + color >> 16, + (color >> 8) & 0xFF, + color & 0xFF, + 0 if transparent else 0xFF, + ) + + def _make_color(self, value, transparent=False): + color = { + "transparent": transparent, + "rgb888": 0, + "rgba": (0, 0, 0, 255), + } + if isinstance(value, (tuple, list, bytes, bytearray)): + value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF + elif isinstance(value, int): + if not 0 <= value <= 0xFFFFFF: + raise ValueError("Color must be between 0x000000 and 0xFFFFFF") + else: + raise TypeError("Color buffer must be a buffer, tuple, list, or int") + color["rgb888"] = value + self._needs_refresh = True + + return color + + def __len__(self): + """Returns the number of colors in a Palette""" + return len(self._colors) + + def __setitem__(self, index, value): + """Sets the pixel color at the given index. The index should be + an integer in the range 0 to color_count-1. + + The value argument represents a color, and can be from 0x000000 to 0xFFFFFF + (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or + 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers. + """ + if self._colors[index]["rgb888"] != value: + self._colors[index] = self._make_color(value) + self._update_rgba(index) + + def __getitem__(self, index): + if not 0 <= index < len(self._colors): + raise ValueError("Palette index out of range") + return self._colors[index] + + def make_transparent(self, palette_index): + """Set the palette index to be a transparent color""" + self._colors[palette_index]["transparent"] = True + self._update_rgba(palette_index) + + def make_opaque(self, palette_index): + """Set the palette index to be an opaque color""" + self._colors[palette_index]["transparent"] = False + self._update_rgba(palette_index) diff --git a/displayio/parallelbus.py b/displayio/parallelbus.py new file mode 100644 index 0000000..f6e75d6 --- /dev/null +++ b/displayio/parallelbus.py @@ -0,0 +1,75 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.parallelbus` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +# pylint: disable=unnecessary-pass, unused-argument + + +class ParallelBus: + """Manage updating a display over 8-bit parallel bus in the background while Python code + runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets. + It doesn’t handle display initialization. + """ + + def __init__(self, i2c_bus, *, device_address, reset=None): + """Create a ParallelBus object associated with the given pins. The + bus is inferred from data0 by implying the next 7 additional pins on a given GPIO + port. + + The parallel bus and pins are then in use by the display until + displayio.release_displays() is called even after a reload. (It does this so + CircuitPython can use the display after your code is done.) So, the first time you + initialize a display bus in code.py you should call + :py:func`displayio.release_displays` first, otherwise it will error after the first + code.py run. + """ + pass + + def reset(self): + """Performs a hardware reset via the reset pin. Raises an exception if called when + no reset pin is available. + """ + pass + + def send(self, command, data): + """Sends the given command value followed by the full set of data. Display state, + such as vertical scroll, set via ``send`` may or may not be reset once the code is + done. + """ + pass diff --git a/displayio/shape.py b/displayio/shape.py new file mode 100644 index 0000000..36f0c04 --- /dev/null +++ b/displayio/shape.py @@ -0,0 +1,61 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.shape` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from displayio.bitmap import Bitmap + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +# pylint: disable=unnecessary-pass, unused-argument + + +class Shape(Bitmap): + """Create a Shape object with the given fixed size. Each pixel is one bit and is stored + by the column boundaries of the shape on each row. Each row’s boundary defaults to the + full row. + """ + + def __init__(self, width, height, *, mirror_x=False, mirror_y=False): + """Create a Shape object with the given fixed size. Each pixel is one bit and is + stored by the column boundaries of the shape on each row. Each row’s boundary + defaults to the full row. + """ + super().__init__(width, height, 2) + + def set_boundary(self, y, start_x, end_x): + """Loads pre-packed data into the given row.""" + pass diff --git a/displayio/test.png b/displayio/test.png new file mode 100644 index 0000000..dca5f07 Binary files /dev/null and b/displayio/test.png differ diff --git a/displayio/tilegrid.py b/displayio/tilegrid.py new file mode 100644 index 0000000..76d3b2c --- /dev/null +++ b/displayio/tilegrid.py @@ -0,0 +1,359 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio.tilegrid` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from recordclass import recordclass +from PIL import Image +from displayio.bitmap import Bitmap +from displayio.colorconverter import ColorConverter +from displayio.ondiskbitmap import OnDiskBitmap +from displayio.shape import Shape +from displayio.palette import Palette + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +Rectangle = recordclass("Rectangle", "x1 y1 x2 y2") +Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y") + +# pylint: disable=too-many-instance-attributes +class TileGrid: + """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple + grids can share bitmaps and pixel shaders. + + A single tile grid is also known as a Sprite. + """ + + def __init__( + self, + bitmap, + *, + pixel_shader, + width=1, + height=1, + tile_width=None, + tile_height=None, + default_tile=0, + x=0, + y=0 + ): + """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is + used to convert the value and its location to a display native pixel color. This may + be a simple color palette lookup, a gradient, a pattern or a color transformer. + + tile_width and tile_height match the height of the bitmap by default. + """ + if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)): + raise ValueError("Unsupported Bitmap type") + self._bitmap = bitmap + bitmap_width = bitmap.width + bitmap_height = bitmap.height + + if not isinstance(pixel_shader, (ColorConverter, Palette)): + raise ValueError("Unsupported Pixel Shader type") + self._pixel_shader = pixel_shader + self._hidden = False + self._x = x + self._y = y + self._width = width # Number of Tiles Wide + self._height = height # Number of Tiles High + self._transpose_xy = False + self._flip_x = False + self._flip_y = False + if tile_width is None: + tile_width = bitmap_width + if tile_height is None: + tile_height = bitmap_height + if bitmap_width % tile_width != 0: + raise ValueError("Tile width must exactly divide bitmap width") + self._tile_width = tile_width + if bitmap_height % tile_height != 0: + raise ValueError("Tile height must exactly divide bitmap height") + self._tile_height = tile_height + if not 0 <= default_tile <= 255: + raise ValueError("Default Tile is out of range") + self._pixel_width = width * tile_width + self._pixel_height = height * tile_height + self._tiles = (self._width * self._height) * [default_tile] + self.in_group = False + self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False) + self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height) + self._moved = False + + def update_transform(self, absolute_transform): + """Update the parent transform and child transforms""" + self._absolute_transform = absolute_transform + if self._absolute_transform is not None: + self._update_current_x() + self._update_current_y() + + def _update_current_x(self): + if self._transpose_xy: + width = self._pixel_height + else: + width = self._pixel_width + if self._absolute_transform.transpose_xy: + self._current_area.y1 = ( + self._absolute_transform.y + self._absolute_transform.dy * self._x + ) + self._current_area.y2 = ( + self._absolute_transform.y + + self._absolute_transform.dy * (self._x + width) + ) + if self._current_area.y2 < self._current_area.y1: + self._current_area.y1, self._current_area.y2 = ( + self._current_area.y2, + self._current_area.y1, + ) + else: + self._current_area.x1 = ( + self._absolute_transform.x + self._absolute_transform.dx * self._x + ) + self._current_area.x2 = ( + self._absolute_transform.x + + self._absolute_transform.dx * (self._x + width) + ) + if self._current_area.x2 < self._current_area.x1: + self._current_area.x1, self._current_area.x2 = ( + self._current_area.x2, + self._current_area.x1, + ) + + def _update_current_y(self): + if self._transpose_xy: + height = self._pixel_width + else: + height = self._pixel_height + if self._absolute_transform.transpose_xy: + self._current_area.x1 = ( + self._absolute_transform.x + self._absolute_transform.dx * self._y + ) + self._current_area.x2 = ( + self._absolute_transform.x + + self._absolute_transform.dx * (self._y + height) + ) + if self._current_area.x2 < self._current_area.x1: + self._current_area.x1, self._current_area.x2 = ( + self._current_area.x2, + self._current_area.x1, + ) + else: + self._current_area.y1 = ( + self._absolute_transform.y + self._absolute_transform.dy * self._y + ) + self._current_area.y2 = ( + self._absolute_transform.y + + self._absolute_transform.dy * (self._y + height) + ) + if self._current_area.y2 < self._current_area.y1: + self._current_area.y1, self._current_area.y2 = ( + self._current_area.y2, + self._current_area.y1, + ) + + # pylint: disable=too-many-locals + def _fill_area(self, buffer): + """Draw onto the image""" + if self._hidden: + return + + image = Image.new( + "RGBA", + (self._width * self._tile_width, self._height * self._tile_height), + (0, 0, 0, 0), + ) + + tile_count_x = self._bitmap.width // self._tile_width + x = self._x + y = self._y + + for tile_x in range(self._width): + for tile_y in range(self._height): + tile_index = self._tiles[tile_y * self._width + tile_x] + tile_index_x = tile_index % tile_count_x + tile_index_y = tile_index // tile_count_x + for pixel_x in range(self._tile_width): + for pixel_y in range(self._tile_height): + image_x = (tile_x * self._tile_width) + pixel_x + image_y = (tile_y * self._tile_height) + pixel_y + bitmap_x = (tile_index_x * self._tile_width) + pixel_x + bitmap_y = (tile_index_y * self._tile_height) + pixel_y + pixel_color = self._pixel_shader[ + self._bitmap[bitmap_x, bitmap_y] + ] + # if not pixel_color["transparent"]: + image.putpixel((image_x, image_y), pixel_color["rgba"]) + + if self._absolute_transform is not None: + if self._absolute_transform.scale > 1: + image = image.resize( + ( + self._pixel_width * self._absolute_transform.scale, + self._pixel_height * self._absolute_transform.scale, + ), + resample=Image.NEAREST, + ) + if self._absolute_transform.mirror_x: + image = image.transpose(Image.FLIP_LEFT_RIGHT) + if self._absolute_transform.mirror_y: + image = image.transpose(Image.FLIP_TOP_BOTTOM) + if self._absolute_transform.transpose_xy: + image = image.transpose(Image.TRANSPOSE) + x *= self._absolute_transform.dx + y *= self._absolute_transform.dy + x += self._absolute_transform.x + y += self._absolute_transform.y + buffer.alpha_composite(image, (x, y)) + + # pylint: enable=too-many-locals + + @property + def hidden(self): + """True when the TileGrid is hidden. This may be False even + when a part of a hidden Group.""" + return self._hidden + + @hidden.setter + def hidden(self, value): + if not isinstance(value, (bool, int)): + raise ValueError("Expecting a boolean or integer value") + self._hidden = bool(value) + + @property + def x(self): + """X position of the left edge in the parent.""" + return self._x + + @x.setter + def x(self, value): + if not isinstance(value, int): + raise TypeError("X should be a integer type") + if self._x != value: + self._x = value + self._update_current_x() + + @property + def y(self): + """Y position of the top edge in the parent.""" + return self._y + + @y.setter + def y(self, value): + if not isinstance(value, int): + raise TypeError("Y should be a integer type") + if self._y != value: + self._y = value + self._update_current_y() + + @property + def flip_x(self): + """If true, the left edge rendered will be the right edge of the right-most tile.""" + return self._flip_x + + @flip_x.setter + def flip_x(self, value): + if not isinstance(value, bool): + raise TypeError("Flip X should be a boolean type") + if self._flip_x != value: + self._flip_x = value + + @property + def flip_y(self): + """If true, the top edge rendered will be the bottom edge of the bottom-most tile.""" + return self._flip_y + + @flip_y.setter + def flip_y(self, value): + if not isinstance(value, bool): + raise TypeError("Flip Y should be a boolean type") + if self._flip_y != value: + self._flip_y = value + + @property + def transpose_xy(self): + """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90 + degree rotation can be achieved along with the corresponding mirrored version. + """ + return self._transpose_xy + + @transpose_xy.setter + def transpose_xy(self, value): + if not isinstance(value, bool): + raise TypeError("Transpose XY should be a boolean type") + if self._transpose_xy != value: + self._transpose_xy = value + self._update_current_x() + self._update_current_y() + + @property + def pixel_shader(self): + """The pixel shader of the tilegrid.""" + return self._pixel_shader + + def __getitem__(self, index): + """Returns the tile index at the given index. The index can either be + an x,y tuple or an int equal to ``y * width + x``'. + """ + if isinstance(index, (tuple, list)): + x = index[0] + y = index[1] + index = y * self._width + x + elif isinstance(index, int): + x = index % self._width + y = index // self._width + if x > self._width or y > self._height or index >= len(self._tiles): + raise ValueError("Tile index out of bounds") + return self._tiles[index] + + def __setitem__(self, index, value): + """Sets the tile index at the given index. The index can either be + an x,y tuple or an int equal to ``y * width + x``. + """ + if isinstance(index, (tuple, list)): + x = index[0] + y = index[1] + index = y * self._width + x + elif isinstance(index, int): + x = index % self._width + y = index // self._width + if x > self._width or y > self._height or index >= len(self._tiles): + raise ValueError("Tile index out of bounds") + if not 0 <= value <= 255: + raise ValueError("Tile value out of bounds") + self._tiles[index] = value + + +# pylint: enable=too-many-instance-attributes diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000..5aca983 Binary files /dev/null and b/docs/_static/favicon.ico differ diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..2fa7117 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,14 @@ + +.. If you created a package, create one automodule per module in the package. + +.. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) +.. use this format as the module name: "adafruit_foo.foo" + +.. automodule:: displayio + :members: + +.. automodule:: fontio + :members: + +.. automodule:: terminalio + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..5528ca6 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- + +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) + +# -- General configuration ------------------------------------------------ + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.todo", +] + +# TODO: Please Read! +# Uncomment the below if you use native CircuitPython modules such as +# digitalio, micropython and busio. List the modules you use. Without it, the +# autodoc module docs will fail to generate with a warning. +# autodoc_mock_imports = ["digitalio", "busio"] + + +intersphinx_mapping = { + "python": ("https://docs.python.org/3.4", None), + "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None), +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "Adafruit_blinka displayio Library" +copyright = "2020 Melissa LeBlanc-Williams" +author = "Melissa LeBlanc-Williams" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = "1.0" +# The full version, including alpha/beta/rc tags. +release = "1.0" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + ".env", + "CODE_OF_CONDUCT.md", +] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +default_role = "any" + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +add_function_parentheses = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + +# If this is True, todo emits a warning for each TODO entries. The default is False. +todo_emit_warnings = True + +napoleon_numpy_docstring = False + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +on_rtd = os.environ.get("READTHEDOCS", None) == "True" + +if not on_rtd: # only import and set the theme if we're building docs locally + try: + import sphinx_rtd_theme + + html_theme = "sphinx_rtd_theme" + html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] + except: + html_theme = "default" + html_theme_path = ["."] +else: + html_theme_path = ["."] + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +html_favicon = "_static/favicon.ico" + +# Output file base name for HTML help builder. +htmlhelp_basename = "Adafruit_blinkaDisplayioLibrarydoc" + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', + # Latex figure (float) alignment + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "Adafruit_blinkadisplayioLibrary.tex", + "Adafruit_blinkadisplayio Library Documentation", + author, + "manual", + ), +] + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ( + master_doc, + "Adafruit_blinkadisplayiolibrary", + "Adafruit_blinka displayio Library Documentation", + [author], + 1, + ), +] + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "Adafruit_blinkadisplayioLibrary", + "Adafruit_blinka displayio Library Documentation", + author, + "Adafruit_blinkadisplayioLibrary", + "One line description of project.", + "Miscellaneous", + ), +] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..26f853a --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,40 @@ +.. include:: ../README.rst + +Table of Contents +================= + +.. toctree:: + :maxdepth: 4 + :hidden: + + self + +.. toctree:: + :caption: API Reference + :maxdepth: 3 + + api + +.. toctree:: + :caption: Tutorials + +.. toctree:: + :caption: Related Products + +.. toctree:: + :caption: Other Links + + Download + CircuitPython Reference Documentation + CircuitPython Support Forum + Discord Chat + Adafruit Learning System + Adafruit Blog + Adafruit Store + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/fontio.py b/fontio.py new file mode 100644 index 0000000..cbb57d6 --- /dev/null +++ b/fontio.py @@ -0,0 +1,117 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`fontio` +================================================================================ + +fontio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from PIL import ImageFont +from displayio import Bitmap + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + + +class BuiltinFont: + """Simulate a font built into CircuitPython""" + + def __init__(self): + self._font = ImageFont.load_default() + self._generate_bitmap(0x20, 0x7E) + + def _generate_bitmap(self, start_range, end_range): + char_width, char_height = self.get_bounding_box() + self._bitmap = Bitmap( + char_width * (end_range - start_range + 1), char_height, 2 + ) + for character in range(start_range, end_range + 1): + ascii_char = chr(character) + ascii_mask = self._font.getmask(ascii_char, mode="1") + for y in range(char_height): + for x in range(char_width): + color = ascii_mask.getpixel((x, y)) + character_position = character - start_range + self._bitmap[character_position * char_width + x, y] = ( + 1 if color else 0 + ) + + def get_bounding_box(self): + """Returns the maximum bounds of all glyphs in the font in + a tuple of two values: width, height. + """ + return self._font.getsize("M") + + def get_glyph(self, codepoint): + """Returns a `fontio.Glyph` for the given codepoint or None if no glyph is available.""" + if 0x20 <= codepoint <= 0x7E: + glyph_index = codepoint - 0x20 + else: + return None + + bounding_box = self._font.getsize(chr(codepoint)) + width, height = bounding_box + return Glyph( + bitmap=self._bitmap, + tile_index=glyph_index, + width=width, + height=height, + dx=0, + dy=0, + shift_x=width, + shift_y=0, + ) + + @property + def bitmap(self): + """Bitmap containing all font glyphs starting with ASCII and followed by unicode. Use + `get_glyph` in most cases. This is useful for use with `displayio.TileGrid` and + `terminalio.Terminal`. + """ + return self._bitmap + + +# pylint: disable=too-few-public-methods, invalid-name +class Glyph: + """Storage of glyph info""" + + def __init__(self, *, bitmap, tile_index, width, height, dx, dy, shift_x, shift_y): + self.bitmap = bitmap + self.width = width + self.height = height + self.dx = dx + self.dy = dy + self.shift_x = shift_x + self.shift_y = shift_y + self.tile_index = tile_index + + +# pylint: enable=too-few-public-methods, invalid-name diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cc712ce --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Adafruit-Blinka +pillow +numpy +recordclass diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e2a138f --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +"""A setuptools based setup module. + +See: +https://packaging.python.org/en/latest/distributing.html +https://github.com/pypa/sampleproject +""" + +from setuptools import setup, find_packages + +# To use a consistent encoding +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, "README.rst"), encoding="utf-8") as f: + long_description = f.read() + +setup( + name="adafruit-blinka-displayio", + use_scm_version=True, + setup_requires=["setuptools_scm"], + description="displayio for Blinka", + long_description=long_description, + long_description_content_type="text/x-rst", + # The project's main homepage. + url="https://github.com/adafruit/Adafruit_Blinka_Displayio", + # Author details + author="Adafruit Industries", + author_email="circuitpython@adafruit.com", + install_requires=["Adafruit-Blinka", "pillow", "numpy", "recordclass",], + # Choose your license + license="MIT", + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Hardware", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + ], + # What does your project relate to? + keywords="adafruit blinka circuitpython micropython displayio lcd tft display pitft", + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + py_modules=["displayio", "fontio", "terminalio"], +) diff --git a/terminalio.py b/terminalio.py new file mode 100644 index 0000000..49ac368 --- /dev/null +++ b/terminalio.py @@ -0,0 +1,48 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`terminalio` +================================================================================ + +terminalio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +import sys # pylint: disable=unused-import +import fontio + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +FONT = fontio.BuiltinFont() + +# TODO: Tap into stdout to get the REPL +# sys.stdout = open('out.dat', 'w') +# sys.stdout.close()