--- /dev/null
+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
--- /dev/null
+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/*
--- /dev/null
+*.mpy
+.idea
+__pycache__
+_build
+*.pyc
+.env
+.python-version
+build*/
+bundles
+*.DS_Store
+.eggs
+dist
+**/*.egg-info
+.vscode
--- /dev/null
+[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*(# )?<?https?://\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
--- /dev/null
+python:
+ version: 3
+requirements_file: requirements.txt
--- /dev/null
+# 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 <support@adafruit.com>.
+
+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
+<https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>,
+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.
--- /dev/null
+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.
+++ /dev/null
-# Adafruit_Blinka_displayio
-Displayio for Blinka
--- /dev/null
+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 <https://github.com/adafruit/Adafruit_Blinka>`_
+
+Installing from PyPI
+=====================
+
+On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
+PyPI <https://pypi.org/project/adafruit-blinka-displayio/>`_. 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
+<https://github.com/adafruit/Adafruit_blinka_CircuitPython_displayio/blob/master/CODE_OF_CONDUCT.md>`_
+before contributing to help this project stay welcoming.
+
+Documentation
+=============
+
+For information on building library documentation, please check out `this guide <https://learn.adafruit.com/creating-and-sharing-a-blinka-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
--- /dev/null
+# 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()
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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()
--- /dev/null
+# 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()
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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)
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+
+.. 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:
--- /dev/null
+# -*- 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",
+ ),
+]
--- /dev/null
+.. 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 <https://github.com/adafruit/Adafruit_Blinka_CircuitPython_displayio/releases/latest>
+ CircuitPython Reference Documentation <https://circuitpython.readthedocs.io>
+ CircuitPython Support Forum <https://forums.adafruit.com/viewforum.php?f=60>
+ Discord Chat <https://adafru.it/discord>
+ Adafruit Learning System <https://learn.adafruit.com>
+ Adafruit Blog <https://blog.adafruit.com>
+ Adafruit Store <https://www.adafruit.com>
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
--- /dev/null
+# 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
--- /dev/null
+Adafruit-Blinka
+pillow
+numpy
+recordclass
--- /dev/null
+"""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"],
+)
--- /dev/null
+# 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()