From 283227b044043c8e2bc67970e426141da8622fc6 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 11 Nov 2020 11:25:04 -0800 Subject: [PATCH 1/9] Initial commit --- .github/workflows/build.yml | 72 +++++ .github/workflows/release.yml | 85 +++++ .gitignore | 18 ++ .pre-commit-config.yaml | 19 ++ .pylintrc | 437 +++++++++++++++++++++++++ .readthedocs.yml | 7 + CODE_OF_CONDUCT.md | 137 ++++++++ LICENSE | 21 ++ LICENSES/CC-BY-4.0.txt | 324 +++++++++++++++++++ LICENSES/MIT.txt | 19 ++ LICENSES/Unlicense.txt | 20 ++ README.md | 2 - README.rst | 78 +++++ README.rst.license | 3 + adafruit_magtag/epaper.py | 90 ++++++ adafruit_magtag/fakerequests.py | 49 +++ adafruit_magtag/graphics.py | 133 ++++++++ adafruit_magtag/magtag.py | 442 +++++++++++++++++++++++++ adafruit_magtag/network.py | 531 +++++++++++++++++++++++++++++++ adafruit_magtag/wifi.py | 91 ++++++ docs/_static/favicon.ico | Bin 0 -> 4414 bytes docs/_static/favicon.ico.license | 3 + docs/api.rst | 8 + docs/api.rst.license | 3 + docs/conf.py | 187 +++++++++++ docs/examples.rst | 8 + docs/examples.rst.license | 3 + docs/index.rst | 51 +++ docs/index.rst.license | 3 + examples/magtag_simpletest.py | 3 + pyproject.toml | 6 + requirements.txt | 11 + setup.py | 63 ++++ 33 files changed, 2925 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100644 .readthedocs.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 LICENSES/CC-BY-4.0.txt create mode 100644 LICENSES/MIT.txt create mode 100644 LICENSES/Unlicense.txt delete mode 100644 README.md create mode 100644 README.rst create mode 100644 README.rst.license create mode 100755 adafruit_magtag/epaper.py create mode 100755 adafruit_magtag/fakerequests.py create mode 100755 adafruit_magtag/graphics.py create mode 100755 adafruit_magtag/magtag.py create mode 100755 adafruit_magtag/network.py create mode 100755 adafruit_magtag/wifi.py create mode 100644 docs/_static/favicon.ico create mode 100644 docs/_static/favicon.ico.license create mode 100644 docs/api.rst create mode 100644 docs/api.rst.license create mode 100644 docs/conf.py create mode 100644 docs/examples.rst create mode 100644 docs/examples.rst.license create mode 100644 docs/index.rst create mode 100644 docs/index.rst.license create mode 100644 examples/magtag_simpletest.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..17b6e2f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +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::$( + echo ${{ github.repository }} | + awk -F '\/' '{ print tolower($2) }' | + tr '_' '-' + ) + - 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, Sphinx, pre-commit + run: | + pip install --force-reinstall pylint Sphinx sphinx-rtd-theme pre-commit + - name: Library version + run: git describe --dirty --always --tags + - name: Pre-commit hooks + run: | + pre-commit run --all-files + - name: PyLint + run: | + pylint $( find . -path './adafruit*.py' ) + ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace $( find . -path "./examples/*.py" )) + - name: Build assets + run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . + - name: Build docs + working-directory: docs + run: sphinx-build -E -W -b html . _build/html + - name: Check For setup.py + id: need-pypi + run: | + echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) + - name: Build Python package + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + run: | + pip install --upgrade setuptools wheel twine readme_renderer testresources + python setup.py sdist + python setup.py bdist_wheel --universal + twine check dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6d0015a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + 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::$( + echo ${{ github.repository }} | + awk -F '\/' '{ print tolower($2) }' | + tr '_' '-' + ) + - 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 deps + run: | + source actions-ci/install.sh + - name: Build assets + run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . + - name: Upload Release Assets + # the 'official' actions version does not yet support dynamically + # supplying asset names to upload. @csexton's version chosen based on + # discussion in the issue below, as its the simplest to implement and + # allows for selecting files with a pattern. + # https://github.com/actions/upload-release-asset/issues/4 + #uses: actions/upload-release-asset@v1.0.1 + uses: csexton/release-asset-action@master + with: + pattern: "bundles/*" + github-token: ${{ secrets.GITHUB_TOKEN }} + + upload-pypi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Check For setup.py + id: need-pypi + run: | + echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) + - name: Set up Python + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + env: + TWINE_USERNAME: ${{ secrets.pypi_username }} + TWINE_PASSWORD: ${{ secrets.pypi_password }} + run: | + python setup.py sdist + twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c6ddfd --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +*.mpy +.idea +__pycache__ +_build +*.pyc +.env +.python-version +build*/ +bundles +*.DS_Store +.eggs +dist +**/*.egg-info +.vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fbda35c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# +# SPDX-License-Identifier: Unlicense + +repos: +- repo: https://github.com/python/black + rev: latest + hooks: + - id: black +- repo: https://github.com/fsfe/reuse-tool + rev: latest + hooks: + - id: reuse +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..54a9d35 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,437 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +# jobs=1 +jobs=2 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +# notes=FIXME,XXX,TODO +notes=FIXME,XXX + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules=board + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +# expected-line-ending-format= +expected-line-ending-format=LF + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +# class-name-hint=[A-Z_][a-zA-Z0-9]+$ +class-name-hint=[A-Z_][a-zA-Z0-9_]+$ + +# Regular expression matching correct class names +# class-rgx=[A-Z_][a-zA-Z0-9]+$ +class-rgx=[A-Z_][a-zA-Z0-9_]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +# good-names=i,j,k,ex,Run,_ +good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +# max-attributes=7 +max-attributes=11 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..a1e2575 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +python: + version: 3 +requirements_file: requirements.txt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d885b36 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,137 @@ + +# Adafruit Community Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and leaders pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level or type of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +We are committed to providing a friendly, safe and welcoming environment for +all. + +Examples of behavior that contributes to creating a positive environment +include: + +* Be kind and courteous to others +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Collaborating with other community members +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and sexual attention or advances +* The use of inappropriate images, including in a community member's avatar +* The use of inappropriate language, including in a community member's nickname +* Any spamming, flaming, baiting or other attention-stealing behavior +* Excessive or unwelcome helping; answering outside the scope of the question + asked +* Trolling, insulting/derogatory comments, and personal or political attacks +* Promoting or spreading disinformation, lies, or conspiracy theories against + a person, group, organisation, project, or community +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate + +The goal of the standards and moderation guidelines outlined here is to build +and maintain a respectful community. We ask that you don’t just aim to be +"technically unimpeachable", but rather try to be your best self. + +We value many things beyond technical expertise, including collaboration and +supporting others within our community. Providing a positive experience for +other community members can have a much more significant impact than simply +providing the correct answer. + +## Our Responsibilities + +Project leaders are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project leaders have the right and responsibility to remove, edit, or +reject messages, comments, commits, code, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any community member for other behaviors that they deem +inappropriate, threatening, offensive, or harmful. + +## Moderation + +Instances of behaviors that violate the Adafruit Community Code of Conduct +may be reported by any member of the community. Community members are +encouraged to report these situations, including situations they witness +involving other community members. + +You may report in the following ways: + +In any situation, you may send an email to . + +On the Adafruit Discord, you may send an open message from any channel +to all Community Moderators by tagging @community moderators. You may +also send an open message from any channel, or a direct message to +@kattni#1507, @tannewt#4653, @danh#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], +version 1.4, available at +, +and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). + +For other projects adopting the Adafruit Community Code of +Conduct, please contact the maintainers of those projects for enforcement. +If you wish to use this code of conduct for your own project, consider +explicitly mentioning your moderation policy or making a copy with your +own moderation policy so as to avoid confusion. + +[Contributor Covenant]: https://www.contributor-covenant.org diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa6d192 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000..3f92dfc --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,324 @@ +Creative Commons Attribution 4.0 International Creative Commons Corporation +("Creative Commons") is not a law firm and does not provide legal services +or legal advice. Distribution of Creative Commons public licenses does not +create a lawyer-client or other relationship. Creative Commons makes its licenses +and related information available on an "as-is" basis. Creative Commons gives +no warranties regarding its licenses, any material licensed under their terms +and conditions, or any related information. Creative Commons disclaims all +liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions +that creators and other rights holders may use to share original works of +authorship and other material subject to copyright and certain other rights +specified in the public license below. The following considerations are for +informational purposes only, are not exhaustive, and do not form part of our +licenses. + +Considerations for licensors: Our public licenses are intended for use by +those authorized to give the public permission to use material in ways otherwise +restricted by copyright and certain other rights. Our licenses are irrevocable. +Licensors should read and understand the terms and conditions of the license +they choose before applying it. Licensors should also secure all rights necessary +before applying our licenses so that the public can reuse the material as +expected. Licensors should clearly mark any material not subject to the license. +This includes other CC-licensed material, or material used under an exception +or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors + +Considerations for the public: By using one of our public licenses, a licensor +grants the public permission to use the licensed material under specified +terms and conditions. If the licensor's permission is not necessary for any +reason–for example, because of any applicable exception or limitation to copyright–then +that use is not regulated by the license. Our licenses grant only permissions +under copyright and certain other rights that a licensor has authority to +grant. Use of the licensed material may still be restricted for other reasons, +including because others have copyright or other rights in the material. A +licensor may make special requests, such as asking that all changes be marked +or described. Although not required by our licenses, you are encouraged to +respect those requests where reasonable. More considerations for the public +: wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution +4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to +be bound by the terms and conditions of this Creative Commons Attribution +4.0 International Public License ("Public License"). To the extent this Public +License may be interpreted as a contract, You are granted the Licensed Rights +in consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the Licensor +receives from making the Licensed Material available under these terms and +conditions. + +Section 1 – Definitions. + +a. Adapted Material means material subject to Copyright and Similar Rights +that is derived from or based upon the Licensed Material and in which the +Licensed Material is translated, altered, arranged, transformed, or otherwise +modified in a manner requiring permission under the Copyright and Similar +Rights held by the Licensor. For purposes of this Public License, where the +Licensed Material is a musical work, performance, or sound recording, Adapted +Material is always produced where the Licensed Material is synched in timed +relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright and Similar +Rights in Your contributions to Adapted Material in accordance with the terms +and conditions of this Public License. + +c. Copyright and Similar Rights means copyright and/or similar rights closely +related to copyright including, without limitation, performance, broadcast, +sound recording, and Sui Generis Database Rights, without regard to how the +rights are labeled or categorized. For purposes of this Public License, the +rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + +d. Effective Technological Measures means those measures that, in the absence +of proper authority, may not be circumvented under laws fulfilling obligations +under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, +and/or similar international agreements. + +e. Exceptions and Limitations means fair use, fair dealing, and/or any other +exception or limitation to Copyright and Similar Rights that applies to Your +use of the Licensed Material. + +f. Licensed Material means the artistic or literary work, database, or other +material to which the Licensor applied this Public License. + +g. Licensed Rights means the rights granted to You subject to the terms and +conditions of this Public License, which are limited to all Copyright and +Similar Rights that apply to Your use of the Licensed Material and that the +Licensor has authority to license. + +h. Licensor means the individual(s) or entity(ies) granting rights under this +Public License. + +i. Share means to provide material to the public by any means or process that +requires permission under the Licensed Rights, such as reproduction, public +display, public performance, distribution, dissemination, communication, or +importation, and to make material available to the public including in ways +that members of the public may access the material from a place and at a time +individually chosen by them. + +j. Sui Generis Database Rights means rights other than copyright resulting +from Directive 96/9/EC of the European Parliament and of the Council of 11 +March 1996 on the legal protection of databases, as amended and/or succeeded, +as well as other essentially equivalent rights anywhere in the world. + +k. You means the individual or entity exercising the Licensed Rights under +this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + +1. Subject to the terms and conditions of this Public License, the Licensor +hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, +irrevocable license to exercise the Licensed Rights in the Licensed Material +to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + +2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions +and Limitations apply to Your use, this Public License does not apply, and +You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + +4. Media and formats; technical modifications allowed. The Licensor authorizes +You to exercise the Licensed Rights in all media and formats whether now known +or hereafter created, and to make technical modifications necessary to do +so. The Licensor waives and/or agrees not to assert any right or authority +to forbid You from making technical modifications necessary to exercise the +Licensed Rights, including technical modifications necessary to circumvent +Effective Technological Measures. For purposes of this Public License, simply +making modifications authorized by this Section 2(a)(4) never produces Adapted +Material. + + 5. Downstream recipients. + +A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed +Material automatically receives an offer from the Licensor to exercise the +Licensed Rights under the terms and conditions of this Public License. + +B. No downstream restrictions. You may not offer or impose any additional +or different terms or conditions on, or apply any Effective Technological +Measures to, the Licensed Material if doing so restricts exercise of the Licensed +Rights by any recipient of the Licensed Material. + +6. No endorsement. Nothing in this Public License constitutes or may be construed +as permission to assert or imply that You are, or that Your use of the Licensed +Material is, connected with, or sponsored, endorsed, or granted official status +by, the Licensor or others designated to receive attribution as provided in +Section 3(a)(1)(A)(i). + + b. Other rights. + +1. Moral rights, such as the right of integrity, are not licensed under this +Public License, nor are publicity, privacy, and/or other similar personality +rights; however, to the extent possible, the Licensor waives and/or agrees +not to assert any such rights held by the Licensor to the limited extent necessary +to allow You to exercise the Licensed Rights, but not otherwise. + +2. Patent and trademark rights are not licensed under this Public License. + +3. To the extent possible, the Licensor waives any right to collect royalties +from You for the exercise of the Licensed Rights, whether directly or through +a collecting society under any voluntary or waivable statutory or compulsory +licensing scheme. In all other cases the Licensor expressly reserves any right +to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following +conditions. + + a. Attribution. + +1. If You Share the Licensed Material (including in modified form), You must: + +A. retain the following if it is supplied by the Licensor with the Licensed +Material: + +i. identification of the creator(s) of the Licensed Material and any others +designated to receive attribution, in any reasonable manner requested by the +Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + +v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + +B. indicate if You modified the Licensed Material and retain an indication +of any previous modifications; and + +C. indicate the Licensed Material is licensed under this Public License, and +include the text of, or the URI or hyperlink to, this Public License. + +2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner +based on the medium, means, and context in which You Share the Licensed Material. +For example, it may be reasonable to satisfy the conditions by providing a +URI or hyperlink to a resource that includes the required information. + +3. If requested by the Licensor, You must remove any of the information required +by Section 3(a)(1)(A) to the extent reasonably practicable. + +4. If You Share Adapted Material You produce, the Adapter's License You apply +must not prevent recipients of the Adapted Material from complying with this +Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to +Your use of the Licensed Material: + +a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, +reuse, reproduce, and Share all or a substantial portion of the contents of +the database; + +b. if You include all or a substantial portion of the database contents in +a database in which You have Sui Generis Database Rights, then the database +in which You have Sui Generis Database Rights (but not its individual contents) +is Adapted Material; and + +c. You must comply with the conditions in Section 3(a) if You Share all or +a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not replace +Your obligations under this Public License where the Licensed Rights include +other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + +a. Unless otherwise separately undertaken by the Licensor, to the extent possible, +the Licensor offers the Licensed Material as-is and as-available, and makes +no representations or warranties of any kind concerning the Licensed Material, +whether express, implied, statutory, or other. This includes, without limitation, +warranties of title, merchantability, fitness for a particular purpose, non-infringement, +absence of latent or other defects, accuracy, or the presence or absence of +errors, whether or not known or discoverable. Where disclaimers of warranties +are not allowed in full or in part, this disclaimer may not apply to You. + +b. To the extent possible, in no event will the Licensor be liable to You +on any legal theory (including, without limitation, negligence) or otherwise +for any direct, special, indirect, incidental, consequential, punitive, exemplary, +or other losses, costs, expenses, or damages arising out of this Public License +or use of the Licensed Material, even if the Licensor has been advised of +the possibility of such losses, costs, expenses, or damages. Where a limitation +of liability is not allowed in full or in part, this limitation may not apply +to You. + +c. The disclaimer of warranties and limitation of liability provided above +shall be interpreted in a manner that, to the extent possible, most closely +approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + +a. This Public License applies for the term of the Copyright and Similar Rights +licensed here. However, if You fail to comply with this Public License, then +Your rights under this Public License terminate automatically. + +b. Where Your right to use the Licensed Material has terminated under Section +6(a), it reinstates: + +1. automatically as of the date the violation is cured, provided it is cured +within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + +c. For the avoidance of doubt, this Section 6(b) does not affect any right +the Licensor may have to seek remedies for Your violations of this Public +License. + +d. For the avoidance of doubt, the Licensor may also offer the Licensed Material +under separate terms or conditions or stop distributing the Licensed Material +at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + +a. The Licensor shall not be bound by any additional or different terms or +conditions communicated by You unless expressly agreed. + +b. Any arrangements, understandings, or agreements regarding the Licensed +Material not stated herein are separate from and independent of the terms +and conditions of this Public License. + +Section 8 – Interpretation. + +a. For the avoidance of doubt, this Public License does not, and shall not +be interpreted to, reduce, limit, restrict, or impose conditions on any use +of the Licensed Material that could lawfully be made without permission under +this Public License. + +b. To the extent possible, if any provision of this Public License is deemed +unenforceable, it shall be automatically reformed to the minimum extent necessary +to make it enforceable. If the provision cannot be reformed, it shall be severed +from this Public License without affecting the enforceability of the remaining +terms and conditions. + +c. No term or condition of this Public License will be waived and no failure +to comply consented to unless expressly agreed to by the Licensor. + +d. Nothing in this Public License constitutes or may be interpreted as a limitation +upon, or waiver of, any privileges and immunities that apply to the Licensor +or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative +Commons may elect to apply one of its public licenses to material it publishes +and in those instances will be considered the "Licensor." The text of the +Creative Commons public licenses is dedicated to the public domain under the +CC0 Public Domain Dedication. Except for the limited purpose of indicating +that material is shared under a Creative Commons public license or as otherwise +permitted by the Creative Commons policies published at creativecommons.org/policies, +Creative Commons does not authorize the use of the trademark "Creative Commons" +or any other trademark or logo of Creative Commons without its prior written +consent including, without limitation, in connection with any unauthorized +modifications to any of its public licenses or any other arrangements, understandings, +or agreements concerning use of licensed material. For the avoidance of doubt, +this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..204b93d --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +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 (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/Unlicense.txt b/LICENSES/Unlicense.txt new file mode 100644 index 0000000..24a8f90 --- /dev/null +++ b/LICENSES/Unlicense.txt @@ -0,0 +1,20 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute +this software, either in source code form or as a compiled binary, for any +purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and +to the detriment of our heirs and successors. We intend this dedication to +be an overt act of relinquishment in perpetuity of all present and future +rights to this software under copyright law. + +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 +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. For more information, +please refer to diff --git a/README.md b/README.md deleted file mode 100644 index 7eb68e7..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Adafruit_CircuitPython_MagTag -Helper library for the Adafruit MagTag diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..ef967aa --- /dev/null +++ b/README.rst @@ -0,0 +1,78 @@ +Introduction +============ + +.. image:: https://readthedocs.org/projects/adafruit-circuitpython-magtag/badge/?version=latest + :target: https://circuitpython.readthedocs.io/projects/magtag/en/latest/ + :alt: Documentation Status + +.. image:: https://img.shields.io/discord/327254708534116352.svg + :target: https://adafru.it/discord + :alt: Discord + +.. image:: https://github.com/adafruit/Adafruit_CircuitPython_MagTag/workflows/Build%20CI/badge.svg + :target: https://github.com/adafruit/Adafruit_CircuitPython_MagTag/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 + +Helper library for the Adafruit MagTag. + + +Dependencies +============= +This driver depends on: + +* `Adafruit CircuitPython `_ + +Please ensure all dependencies are available on the CircuitPython filesystem. +This is easily achieved by downloading +`the Adafruit library and driver bundle `_. + +Installing from PyPI +===================== +.. note:: This library is not available on PyPI yet. Install documentation is included + as a standard element. Stay tuned for PyPI availability! + +.. todo:: Remove the above note if PyPI version is/will be available at time of release. + If the library is not planned for PyPI, remove the entire 'Installing from PyPI' section. + +On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from +PyPI `_. To install for current user: + +.. code-block:: shell + + pip3 install adafruit-circuitpython-magtag + +To install system-wide (this may be required in some cases): + +.. code-block:: shell + + sudo pip3 install adafruit-circuitpython-magtag + +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-circuitpython-magtag + +Usage Example +============= + +.. todo:: Add a quick, simple example. It and other examples should live in the examples folder and be included in docs/examples.rst. + +Contributing +============ + +Contributions are welcome! Please read our `Code of Conduct +`_ +before contributing to help this project stay welcoming. + +Documentation +============= + +For information on building library documentation, please check out `this guide `_. diff --git a/README.rst.license b/README.rst.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/README.rst.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/adafruit_magtag/epaper.py b/adafruit_magtag/epaper.py new file mode 100755 index 0000000..e85ef1e --- /dev/null +++ b/adafruit_magtag/epaper.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_magtag.epaper` +================================================================================ + +Helper Library for the Adafruit MagTag. + + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit MagTag `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import board +from time import sleep +import displayio +import adafruit_il0373 + +MAGTAG_29_GRAYSCALE = 1 + +DISPLAY_TYPE_MONO = 1 +DISPLAY_TYPE_TRICOLOR = 2 +DISPLAY_TYPE_GRAYSCALE = 3 + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" + +class EPaper: + """Class representing the EPaper Display. This is used to automatically + initialize the display. + + :param int width: The width of the display in Pixels. Defaults to 64. + :param int height: The height of the display in Pixels. Defaults to 32. + :param int rotation: The degrees to rotate the display. Defaults to 270. + """ + + # pylint: disable=too-few-public-methods,too-many-branches + def __init__( + self, + profile=MAGTAG_29_GRAYSCALE, + rotation=270, + ): + display_type = None + self._refresh_time = 5 + + if profile == MAGTAG_29_GRAYSCALE: + width = 296 + height = 128 + display_type = DISPLAY_TYPE_GRAYSCALE + else: + raise ValueError("Unknown MagTag Profile") + + try: + displayio.release_displays() + display_bus = displayio.FourWire( + board.SPI(), command=board.EPD_DC, chip_select=board.EPD_CS, reset=board.EPD_RESET, baudrate=1000000 + ) + sleep(1) + if display_type == DISPLAY_TYPE_GRAYSCALE: + self.display = adafruit_il0373.IL0373( + display_bus, + width=width, + height=height, + rotation=rotation, + black_bits_inverted=False, + color_bits_inverted=False, + grayscale=True, + refresh_time=1, + seconds_per_frame=self._refresh_time, + ) + except ValueError: + raise RuntimeError("Failed to initialize ePaper Display") from ValueError + + @property + def refresh_time(self): + return self._refresh_time diff --git a/adafruit_magtag/fakerequests.py b/adafruit_magtag/fakerequests.py new file mode 100755 index 0000000..d30a020 --- /dev/null +++ b/adafruit_magtag/fakerequests.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_magtag.fakerequests` +================================================================================ + +Helper Library for the Adafruit MagTag. + + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit MagTag `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import json + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" + + +class Fake_Requests: + """For faking 'requests' using a local file instead of the network.""" + + def __init__(self, filename): + self._filename = filename + + def json(self): + """json parsed version for local requests.""" + with open(self._filename, "r") as file: + return json.load(file) + + @property + def text(self): + """raw text version for local requests.""" + with open(self._filename, "r") as file: + return file.read() diff --git a/adafruit_magtag/graphics.py b/adafruit_magtag/graphics.py new file mode 100755 index 0000000..7576c92 --- /dev/null +++ b/adafruit_magtag/graphics.py @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_magtag.graphics` +================================================================================ + +Helper Library for the Adafruit MagTag. + + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit MagTag `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import gc +from time import sleep +import displayio +from adafruit_magtag.epaper import EPaper, MAGTAG_29_GRAYSCALE + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" + + +class Graphics: + """Graphics Helper Class for the MagTag Library + + :param default_bg: The path to your default background image file or a hex color. + Defaults to 0x000000. + :param int width: The width of the display in Pixels. Defaults to 64. + :param int height: The height of the display in Pixels. Defaults to 32. + :param int bit_depth: The number of bits per color channel. Defaults to 2. + :param list alt_addr_pins: An alternate set of address pins to use. Defaults to None + :param string color_order: A string containing the letter "R", "G", and "B" in the + order you want. Defaults to "RGB" + :param debug: Turn on debug print outs. Defaults to False. + + """ + + # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements + def __init__( + self, + *, + default_bg=None, + profile=MAGTAG_29_GRAYSCALE, + rotation=270, + debug=False + ): + + self._debug = debug + epaper = EPaper( + profile=profile, + rotation=rotation, + ) + self.display = epaper.display + + if self._debug: + print("Init display") + self.splash = displayio.Group(max_size=15) + + if self._debug: + print("Init background") + self._bg_group = displayio.Group(max_size=1) + self._bg_file = None + self.splash.append(self._bg_group) + + # set the default background + if default_bg is not None: + self.set_background(default_bg) + self.display.show(self.splash) + + gc.collect() + + def set_background(self, file_or_color, position=None): + """The background image to a bitmap file. + + :param file_or_color: The filename of the chosen background image, or a hex color. + + """ + print("Set background to ", file_or_color) + while self._bg_group: + self._bg_group.pop() + + if not position: + position = (0, 0) # default in top corner + + if not file_or_color: + return # we're done, no background desired + if self._bg_file: + self._bg_file.close() + if isinstance(file_or_color, str): # its a filenme: + self._bg_file = open(file_or_color, "rb") + background = displayio.OnDiskBitmap(self._bg_file) + self._bg_sprite = displayio.TileGrid( + background, + pixel_shader=displayio.ColorConverter(), + x=position[0], + y=position[1], + ) + elif isinstance(file_or_color, int): + # Make a background color fill + color_bitmap = displayio.Bitmap(self.display.width, self.display.height, 1) + color_palette = displayio.Palette(1) + color_palette[0] = file_or_color + self._bg_sprite = displayio.TileGrid( + color_bitmap, pixel_shader=color_palette, x=position[0], y=position[1], + ) + else: + raise RuntimeError("Unknown type of background") + self._bg_group.append(self._bg_sprite) + self.display.refresh() + sleep(self.epaper.refresh_time) + gc.collect() + + def qrcode(): + """ + + + + """ + pass diff --git a/adafruit_magtag/magtag.py b/adafruit_magtag/magtag.py new file mode 100755 index 0000000..8e09f60 --- /dev/null +++ b/adafruit_magtag/magtag.py @@ -0,0 +1,442 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_magtag.magtag` +================================================================================ + +Helper library for the Adafruit MagTag. + + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit MagTag `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import gc +from time import sleep +import terminalio +from adafruit_bitmap_font import bitmap_font +from adafruit_display_text.label import Label +from adafruit_magtag.network import Network +from adafruit_magtag.graphics import Graphics + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" + + +class MagTag: + """Class representing the Adafruit MagTag. + + :param url: The URL of your data source. Defaults to ``None``. + :param headers: The headers for authentication, typically used by Azure API's. + :param json_path: The list of json traversal to get data out of. Can be list of lists for + multiple data points. Defaults to ``None`` to not use json. + :param regexp_path: The list of regexp strings to get data out (use a single regexp group). Can + be list of regexps for multiple data points. Defaults to ``None`` to not + use regexp. + :param default_bg: The path to your default background image file or a hex color. + Defaults to 0x000000. + :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board + NeoPixel. Defaults to ``None``, not the status LED + :param json_transform: A function or a list of functions to call with the parsed JSON. + Changes and additions are permitted for the ``dict`` object. + :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used + before calling the pyportal class. Defaults to ``None``. + :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. + :param int bit_depth: The number of bits per color channel. Defaults to 2. + :param list alt_addr_pins: An alternate set of address pins to use. Defaults to None + :param string color_order: A string containing the letter "R", "G", and "B" in the + order you want. Defaults to "RGB" + :param debug: Turn on debug print outs. Defaults to False. + + """ + + # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements + def __init__( + self, + *, + url=None, + headers=None, + json_path=None, + regexp_path=None, + default_bg=0x000000, + status_neopixel=None, + json_transform=None, + esp=None, + external_spi=None, + bit_depth=2, + alt_addr_pins=None, + color_order="RGB", + debug=False, + width=64, + height=32, + ): + + self._debug = debug + self.graphics = Graphics( + default_bg=default_bg, + bit_depth=bit_depth, + width=width, + height=height, + alt_addr_pins=alt_addr_pins, + color_order=color_order, + debug=debug, + ) + self.display = self.graphics.display + + self.network = Network( + status_neopixel=status_neopixel, + esp=esp, + external_spi=external_spi, + extract_values=False, + debug=debug, + ) + + self._url = None + self.url = url + self._headers = headers + self._json_path = None + self.json_path = json_path + + self._regexp_path = regexp_path + + self.splash = self.graphics.splash + + # Add any JSON translators + if json_transform: + self.network.add_json_transform(json_transform) + + self._text = [] + self._text_color = [] + self._text_position = [] + self._text_wrap = [] + self._text_maxlen = [] + self._text_transform = [] + self._text_scrolling = [] + self._text_scale = [] + self._scrolling_index = None + self._text_font = [] + self._text_line_spacing = [] + + gc.collect() + + # pylint: disable=too-many-arguments + def add_text( + self, + text_position=None, + text_font=terminalio.FONT, + text_color=0x808080, + text_wrap=False, + text_maxlen=0, + text_transform=None, + text_scale=1, + scrolling=False, + line_spacing=1.25, + ): + """ + Add text labels with settings + + :param str text_font: The path to your font file for your data text display. + :param text_position: The position of your extracted text on the display in an (x, y) tuple. + Can be a list of tuples for when there's a list of json_paths, for + example. + :param text_color: The color of the text, in 0xRRGGBB format. Can be a list of colors for + when there's multiple texts. Defaults to ``None``. + :param text_wrap: Whether or not to wrap text (for long text data chunks). Defaults to + ``False``, no wrapping. + :param text_maxlen: The max length of the text for text wrapping. Defaults to 0. + :param text_transform: A function that will be called on the text before display + :param int text_scale: The factor to scale the default size of the text by + :param bool scrolling: If true, text is placed offscreen and the scroll() function is used + to scroll text on a pixel-by-pixel basis. Multiple text labels with + the scrolling set to True will be cycled through. + + """ + if text_font is terminalio.FONT: + self._text_font.append(text_font) + else: + self._text_font.append(bitmap_font.load_font(text_font)) + if not text_wrap: + text_wrap = 0 + if not text_maxlen: + text_maxlen = 0 + if not text_transform: + text_transform = None + if not isinstance(text_scale, (int, float)) or text_scale < 1: + text_scale = 1 + text_scale = round(text_scale) + if scrolling: + if text_position is None: + # Center text if position not specified + text_position = (self.display.width, self.display.height // 2 - 1) + else: + text_position = (self.display.width, text_position[1]) + + gc.collect() + + if self._debug: + print("Init text area") + self._text.append(None) + self._text_color.append(self.html_color_convert(text_color)) + self._text_position.append(text_position) + self._text_wrap.append(text_wrap) + self._text_maxlen.append(text_maxlen) + self._text_transform.append(text_transform) + self._text_scale.append(text_scale) + self._text_scrolling.append(scrolling) + self._text_line_spacing.append(line_spacing) + + if scrolling and self._scrolling_index is None: # Not initialized yet + self._scrolling_index = self._get_next_scrollable_text_index() + + # pylint: enable=too-many-arguments + + @staticmethod + def html_color_convert(color): + """Convert an HTML color code to an integer + + :param color: The color value to be converted + + """ + if isinstance(color, str): + if color[0] == "#": + color = color.lstrip("#") + return int(color, 16) + return color # Return unconverted + + def set_headers(self, headers): + """Set the headers used by fetch(). + + :param headers: The new header dictionary + + """ + self._headers = headers + + def set_background(self, file_or_color, position=None): + """The background image to a bitmap file. + + :param file_or_color: The filename of the chosen background image, or a hex color. + + """ + self.graphics.set_background(file_or_color, position) + + def preload_font(self, glyphs=None, index=0): + # pylint: disable=line-too-long + """Preload font. + + :param glyphs: The font glyphs to load. Defaults to ``None``, uses alphanumeric glyphs if + None. + """ + # pylint: enable=line-too-long + if not glyphs: + glyphs = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-!,. \"'?!" + print("Preloading font glyphs:", glyphs) + if self._text_font[index] is not terminalio.FONT: + self._text_font[index].load_glyphs(glyphs) + + def set_text_color(self, color, index=0): + """Update the text color, with indexing into our list of text boxes. + + :param int color: The color value to be used + :param index: Defaults to 0. + + """ + if self._text[index]: + color = self.html_color_convert(color) + self._text_color[index] = color + self._text[index].color = color + + def set_text(self, val, index=0): + """Display text, with indexing into our list of text boxes. + + :param str val: The text to be displayed + :param index: Defaults to 0. + + """ + # Make sure at least a single label exists + if not self._text: + self.add_text() + string = str(val) + if self._text_maxlen[index]: + string = string[: self._text_maxlen[index]] + print("text index", self._text[index]) + index_in_splash = None + + if self._text[index] is not None: + if self._debug: + print("Replacing text area with :", string) + index_in_splash = self.splash.index(self._text[index]) + elif self._debug: + print("Creating text area with :", string) + + if len(string) > 0: + self._text[index] = Label( + self._text_font[index], text=string, scale=self._text_scale[index] + ) + self._text[index].color = self._text_color[index] + self._text[index].x = self._text_position[index][0] + self._text[index].y = self._text_position[index][1] + self._text[index].line_spacing = self._text_line_spacing[index] + elif index_in_splash is not None: + self._text[index] = None + + if index_in_splash is not None: + if self._text[index] is not None: + self.splash[index_in_splash] = self._text[index] + else: + del self.splash[index_in_splash] + elif self._text[index] is not None: + self.splash.append(self._text[index]) + + def get_local_time(self, location=None): + """Accessor function for get_local_time()""" + return self.network.get_local_time(location=location) + + def push_to_io(self, feed_key, data): + """Push data to an adafruit.io feed + + :param str feed_key: Name of feed key to push data to. + :param data: data to send to feed + + """ + + self.network.push_to_io(feed_key, data) + + def get_io_data(self, feed_key): + """Return all values from the Adafruit IO Feed Data that matches the feed key + + :param str feed_key: Name of feed key to receive data from. + + """ + + return self.network.get_io_data(feed_key) + + def get_io_feed(self, feed_key, detailed=False): + """Return the Adafruit IO Feed that matches the feed key + + :param str feed_key: Name of feed key to match. + :param bool detailed: Whether to return additional detailed information + + """ + return self.network.get_io_feed(feed_key, detailed) + + def get_io_group(self, group_key): + """Return the Adafruit IO Group that matches the group key + + :param str group_key: Name of group key to match. + + """ + return self.network.get_io_group(group_key) + + + def fetch(self, refresh_url=None, timeout=10): + """Fetch data from the url we initialized with, perfom any parsing, + and display text or graphics. This function does pretty much everything + Optionally update the URL + """ + if refresh_url: + self._url = refresh_url + values = [] + + values = self.network.fetch_data( + self._url, + headers=self._headers, + json_path=self._json_path, + regexp_path=self._regexp_path, + timeout=timeout, + ) + + # fill out all the text blocks + if self._text: + for i in range(len(self._text)): + string = None + if self._text_transform[i]: + func = self._text_transform[i] + string = func(values[i]) + else: + try: + string = "{:,d}".format(int(values[i])) + except (TypeError, ValueError): + string = values[i] # ok its a string + if self._debug: + print("Drawing text", string) + if self._text_wrap[i]: + if self._debug: + print("Wrapping text") + lines = self.wrap_nicely(string, self._text_wrap[i]) + string = "\n".join(lines) + self.set_text(string, index=i) + if len(values) == 1: + return values[0] + return values + + # return a list of lines with wordwrapping + @staticmethod + def wrap_nicely(string, max_chars): + """A helper that will return a list of lines with word-break wrapping. + + :param str string: The text to be wrapped. + :param int max_chars: The maximum number of characters on a line before wrapping. + + """ + string = string.replace("\n", "").replace("\r", "") # strip confusing newlines + words = string.split(" ") + the_lines = [] + the_line = "" + for w in words: + if len(the_line + " " + w) <= max_chars: + the_line += " " + w + else: + the_lines.append(the_line) + the_line = "" + w + if the_line: # last line remaining + the_lines.append(the_line) + # remove first space from first line: + the_lines[0] = the_lines[0][1:] + return the_lines + + @property + def url(self): + """ + Get or set the URL of your data source. + """ + return self._json_path + + @url.setter + def url(self, value): + self._url = value + if value and not self.network.uselocal: + self.network.connect() + if self._debug: + print("My IP address is", self.network.ip_address) + + @property + def json_path(self): + """ + Get or set the list of json traversal to get data out of. Can be list + of lists for multiple data points. + """ + return self._json_path + + @json_path.setter + def json_path(self, value): + if value: + if isinstance(value[0], (list, tuple)): + self._json_path = value + else: + self._json_path = (value,) + else: + self._json_path = None diff --git a/adafruit_magtag/network.py b/adafruit_magtag/network.py new file mode 100755 index 0000000..0cc23ca --- /dev/null +++ b/adafruit_magtag/network.py @@ -0,0 +1,531 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_magtag.network` +================================================================================ + +Helper library for the Adafruit MagTag. + + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit MagTag `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import os +import time +import gc +from micropython import const +from adafruit_io.adafruit_io import IO_HTTP, AdafruitIO_RequestError +import supervisor +import rtc +from adafruit_magtag.wifi import WiFi +from adafruit_magtag.fakerequests import Fake_Requests + + +try: + from secrets import secrets +except ImportError: + print( + """WiFi settings are kept in secrets.py, please add them there! +the secrets dictionary must contain 'ssid' and 'password' at a minimum""" + ) + raise + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" + +# pylint: disable=line-too-long +# pylint: disable=too-many-lines +# you'll need to pass in an io username and key +TIME_SERVICE = ( + "https://io.adafruit.com/api/v2/%s/integrations/time/strftime?x-aio-key=%s" +) +# our strftime is %Y-%m-%d %H:%M:%S.%L %j %u %z %Z see http://strftime.net/ for decoding details +# See https://apidock.com/ruby/DateTime/strftime for full options +TIME_SERVICE_STRFTIME = ( + "&fmt=%25Y-%25m-%25d+%25H%3A%25M%3A%25S.%25L+%25j+%25u+%25z+%25Z" +) +LOCALFILE = "local.txt" +# pylint: enable=line-too-long + +STATUS_NO_CONNECTION = (100, 0, 0) +STATUS_CONNECTING = (0, 0, 100) +STATUS_FETCHING = (200, 100, 0) +STATUS_DOWNLOADING = (0, 100, 100) +STATUS_CONNECTED = (0, 100, 0) +STATUS_DATA_RECEIVED = (0, 0, 100) +STATUS_OFF = (0, 0, 0) + +CONTENT_TEXT = const(1) +CONTENT_JSON = const(2) +CONTENT_IMAGE = const(3) + + +class HttpError(Exception): + """HTTP Specific Error""" + + +class Network: + """Class representing the Adafruit RGB Matrix Portal. + + :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board + NeoPixel. Defaults to ``None``, not the status LED + :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used + before calling the pyportal class. Defaults to ``None``. + :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. + :param bool extract_values: If true, single-length fetched values are automatically extracted + from lists and tuples. Defaults to ``True``. + :param debug: Turn on debug print outs. Defaults to False. + + """ + + # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements + def __init__( + self, + *, + status_neopixel=None, + esp=None, + external_spi=None, + extract_values=True, + debug=False, + ): + self._wifi = WiFi( + status_neopixel=status_neopixel, esp=esp, external_spi=external_spi + ) + self._debug = debug + self.json_transform = [] + self._extract_values = extract_values + + # This may be removed. Using for testing + self.requests = None + + try: + os.stat(LOCALFILE) + self.uselocal = True + except OSError: + self.uselocal = False + + gc.collect() + + def neo_status(self, value): + """The status NeoPixel. + + :param value: The color to change the NeoPixel. + + """ + self._wifi.neo_status(value) + + @staticmethod + def json_traverse(json, path): + """ + Traverse down the specified JSON path and return the value or values + + :param json: JSON data to traverse + :param list path: The path that we want to follow + + """ + value = json + if not isinstance(path, (list, tuple)): + raise ValueError( + "The json_path parameter should be enclosed in a list or tuple." + ) + for x in path: + value = value[x] + gc.collect() + return value + + def add_json_transform(self, json_transform): + """Add a function that is applied to JSON data when data is fetched + + :param json_transform: A function or a list of functions to call with the parsed JSON. + Changes and additions are permitted for the ``dict`` object. + """ + if callable(json_transform): + self.json_transform.append(json_transform) + else: + self.json_transform.extend(filter(callable, json_transform)) + + def get_local_time(self, location=None): + # pylint: disable=line-too-long + """Fetch and "set" the local time of this microcontroller to the local time at the location, using an internet time API. + + :param str location: Your city and country, e.g. ``"New York, US"``. + + """ + # pylint: enable=line-too-long + self.connect() + api_url = None + try: + aio_username = secrets["aio_username"] + aio_key = secrets["aio_key"] + except KeyError: + raise KeyError( + "\n\nOur time service requires a login/password to rate-limit. Please register for a free adafruit.io account and place the user/key in your secrets file under 'aio_username' and 'aio_key'" # pylint: disable=line-too-long + ) from KeyError + + location = secrets.get("timezone", location) + if location: + print("Getting time for timezone", location) + api_url = (TIME_SERVICE + "&tz=%s") % (aio_username, aio_key, location) + else: # we'll try to figure it out from the IP address + print("Getting time from IP address") + api_url = TIME_SERVICE % (aio_username, aio_key) + api_url += TIME_SERVICE_STRFTIME + try: + response = self._wifi.requests.get(api_url, timeout=10) + if response.status_code != 200: + error_message = ( + "Error connection to Adafruit IO. The response was: " + + response.text + ) + raise ValueError(error_message) + if self._debug: + print("Time request: ", api_url) + print("Time reply: ", response.text) + times = response.text.split(" ") + the_date = times[0] + the_time = times[1] + year_day = int(times[2]) + week_day = int(times[3]) + is_dst = None # no way to know yet + except KeyError: + raise KeyError( + "Was unable to lookup the time, try setting secrets['timezone'] according to http://worldtimeapi.org/timezones" # pylint: disable=line-too-long + ) from KeyError + year, month, mday = [int(x) for x in the_date.split("-")] + the_time = the_time.split(".")[0] + hours, minutes, seconds = [int(x) for x in the_time.split(":")] + now = time.struct_time( + (year, month, mday, hours, minutes, seconds, week_day, year_day, is_dst) + ) + rtc.RTC().datetime = now + + # now clean up + response.close() + response = None + gc.collect() + + def wget(self, url, filename, *, chunk_size=12000): + """Download a url and save to filename location, like the command wget. + + :param url: The URL from which to obtain the data. + :param filename: The name of the file to save the data to. + :param chunk_size: how much data to read/write at a time. + + """ + print("Fetching stream from", url) + + self.neo_status(STATUS_FETCHING) + response = self._wifi.requests.get(url, stream=True) + + headers = {} + for title, content in response.headers.items(): + headers[title.lower()] = content + + if response.status_code == 200: + print("Reply is OK!") + self.neo_status((0, 0, 100)) # green = got data + else: + if self._debug: + if "content-length" in headers: + print("Content-Length: {}".format(int(headers["content-length"]))) + if "date" in headers: + print("Date: {}".format(headers["date"])) + self.neo_status((100, 0, 0)) # red = http error + raise HttpError( + "Code {}: {}".format( + response.status_code, response.reason.decode("utf-8") + ) + ) + + if self._debug: + print(response.headers) + if "content-length" in headers: + content_length = int(headers["content-length"]) + else: + raise RuntimeError("Content-Length missing from headers") + remaining = content_length + print("Saving data to ", filename) + stamp = time.monotonic() + file = open(filename, "wb") + for i in response.iter_content(min(remaining, chunk_size)): # huge chunks! + self.neo_status(STATUS_DOWNLOADING) + remaining -= len(i) + file.write(i) + if self._debug: + print( + "Read %d bytes, %d remaining" + % (content_length - remaining, remaining) + ) + else: + print(".", end="") + if not remaining: + break + self.neo_status(STATUS_FETCHING) + file.close() + + response.close() + stamp = time.monotonic() - stamp + print( + "Created file of %d bytes in %0.1f seconds" % (os.stat(filename)[6], stamp) + ) + self.neo_status(STATUS_OFF) + if not content_length == os.stat(filename)[6]: + raise RuntimeError + + def connect(self): + """ + Connect to WiFi using the settings found in secrets.py + """ + self._wifi.neo_status(STATUS_CONNECTING) + while not self._wifi.is_connected: + # secrets dictionary must contain 'ssid' and 'password' at a minimum + print("Connecting to AP", secrets["ssid"]) + if secrets["ssid"] == "CHANGE ME" or secrets["password"] == "CHANGE ME": + change_me = "\n" + "*" * 45 + change_me += "\nPlease update the 'secrets.py' file on your\n" + change_me += "CIRCUITPY drive to include your local WiFi\n" + change_me += "access point SSID name in 'ssid' and SSID\n" + change_me += "password in 'password'. Then save to reload!\n" + change_me += "*" * 45 + raise OSError(change_me) + self._wifi.neo_status(STATUS_NO_CONNECTION) # red = not connected + try: + self._wifi.connect(secrets['ssid'], secrets['password']) + self.requests = self._wifi.requests + except RuntimeError as error: + print("Could not connect to internet", error) + print("Retrying in 3 seconds...") + time.sleep(3) + + def _get_io_client(self): + self.connect() + + try: + aio_username = secrets["aio_username"] + aio_key = secrets["aio_key"] + except KeyError: + raise KeyError( + "Adafruit IO secrets are kept in secrets.py, please add them there!\n\n" + ) from KeyError + + return IO_HTTP(aio_username, aio_key, self._wifi.manager(secrets)) + + def push_to_io(self, feed_key, data): + """Push data to an adafruit.io feed + + :param str feed_key: Name of feed key to push data to. + :param data: data to send to feed + + """ + + io_client = self._get_io_client() + + while True: + try: + feed_id = io_client.get_feed(feed_key) + except AdafruitIO_RequestError: + # If no feed exists, create one + feed_id = io_client.create_new_feed(feed_key) + except RuntimeError as exception: + print("An error occured, retrying! 1 -", exception) + continue + break + + while True: + try: + io_client.send_data(feed_id["key"], data) + except RuntimeError as exception: + print("An error occured, retrying! 2 -", exception) + continue + except NameError as exception: + print(feed_id["key"], data, exception) + continue + break + + def get_io_feed(self, feed_key, detailed=False): + """Return the Adafruit IO Feed that matches the feed key + + :param str feed_key: Name of feed key to match. + :param bool detailed: Whether to return additional detailed information + + """ + io_client = self._get_io_client() + + while True: + try: + return io_client.get_feed(feed_key, detailed=detailed) + except RuntimeError as exception: + print("An error occured, retrying! 1 -", exception) + continue + break + + def get_io_group(self, group_key): + """Return the Adafruit IO Group that matches the group key + + :param str group_key: Name of group key to match. + + """ + io_client = self._get_io_client() + + while True: + try: + return io_client.get_group(group_key) + except RuntimeError as exception: + print("An error occured, retrying! 1 -", exception) + continue + break + + def get_io_data(self, feed_key): + """Return all values from Adafruit IO Feed Data that matches the feed key + + :param str feed_key: Name of feed key to receive data from. + + """ + io_client = self._get_io_client() + + while True: + try: + return io_client.receive_all_data(feed_key) + except RuntimeError as exception: + print("An error occured, retrying! 1 -", exception) + continue + break + + def fetch(self, url, *, headers=None, timeout=10): + """Fetch data from the specified url and return a response object""" + gc.collect() + if self._debug: + print("Free mem: ", gc.mem_free()) # pylint: disable=no-member + + response = None + if self.uselocal: + print("*** USING LOCALFILE FOR DATA - NOT INTERNET!!! ***") + response = Fake_Requests(LOCALFILE) + + if not response: + self.connect() + # great, lets get the data + print("Retrieving data...", end="") + self.neo_status(STATUS_FETCHING) # yellow = fetching data + gc.collect() + response = self._wifi.requests.get(url, headers=headers, timeout=timeout) + gc.collect() + + return response + + def fetch_data( + self, url, *, headers=None, json_path=None, regexp_path=None, timeout=10, + ): + """Fetch data from the specified url and perfom any parsing""" + json_out = None + values = [] + content_type = CONTENT_TEXT + + response = self.fetch(url, headers=headers, timeout=timeout) + + headers = {} + for title, content in response.headers.items(): + headers[title.lower()] = content + gc.collect() + if self._debug: + print("Headers:", headers) + if response.status_code == 200: + print("Reply is OK!") + self.neo_status(STATUS_DATA_RECEIVED) # green = got data + if "content-type" in headers: + if "image/" in headers["content-type"]: + content_type = CONTENT_IMAGE + elif "application/json" in headers["content-type"]: + content_type = CONTENT_JSON + elif "application/javascript" in headers["content-type"]: + content_type = CONTENT_JSON + else: + if self._debug: + if "content-length" in headers: + print("Content-Length: {}".format(int(headers["content-length"]))) + if "date" in headers: + print("Date: {}".format(headers["date"])) + self.neo_status((100, 0, 0)) # red = http error + raise HttpError( + "Code {}: {}".format( + response.status_code, response.reason.decode("utf-8") + ) + ) + + if content_type == CONTENT_JSON and json_path is not None: + if isinstance(json_path, (list, tuple)) and ( + not json_path or not isinstance(json_path[0], (list, tuple)) + ): + json_path = (json_path,) + try: + gc.collect() + json_out = response.json() + if self._debug: + print(json_out) + gc.collect() + except ValueError: # failed to parse? + print("Couldn't parse json: ", response.text) + raise + except MemoryError: + supervisor.reload() + + if regexp_path: + import re # pylint: disable=import-outside-toplevel + + # optional JSON post processing, apply any transformations + # these MAY change/add element + for idx, json_transform in enumerate(self.json_transform): + try: + json_transform(json_out) + except Exception as error: + print("Exception from json_transform: ", idx, error) + raise + + # extract desired text/values from json + if json_out and json_path: + for path in json_path: + try: + values.append(self.json_traverse(json_out, path)) + except KeyError: + print(json_out) + raise + elif content_type == CONTENT_TEXT and regexp_path: + for regexp in regexp_path: + values.append(re.search(regexp, response.text).group(1)) + else: + if json_out: + # No path given, so return JSON as string for compatibility + import json # pylint: disable=import-outside-toplevel + + values = json.dumps(response.json()) + else: + values = response.text + + # we're done with the requests object, lets delete it so we can do more! + json_out = None + response = None + gc.collect() + if self._extract_values and len(values) == 1: + return values[0] + + return values + + @property + def ip_address(self): + """Return the IP Address nicely formatted""" + return self._wifi.ip_address diff --git a/adafruit_magtag/wifi.py b/adafruit_magtag/wifi.py new file mode 100755 index 0000000..334cb56 --- /dev/null +++ b/adafruit_magtag/wifi.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_magtag.wifi` +================================================================================ + +Helper Library for the Adafruit MagTag. + + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit Metro M4 Express AirLift `_ +* `Adafruit RGB Matrix Shield `_ +* `64x32 RGB LED Matrix `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import gc +import board +import busio +from digitalio import DigitalInOut +import neopixel +import ipaddress +import wifi +import socketpool +import adafruit_requests +import ssl +import espidf + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" + + +class WiFi: + """Class representing the ESP. + + :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board + NeoPixel. Defaults to ``None``, not the status LED + :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used + before calling the pyportal class. Defaults to ``None``. + :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. + + """ + + def __init__(self, *, status_neopixel=None, esp=None, external_spi=None): + + if status_neopixel: + self.neopix = neopixel.NeoPixel(status_neopixel, 1, brightness=0.2) + else: + self.neopix = None + self.neo_status(0) + self.requests = None + self._connected = False + + gc.collect() + + def connect(self, ssid, password): + print(ssid, password) + wifi.radio.connect(ssid, password) + pool = socketpool.SocketPool(wifi.radio) + self.requests = adafruit_requests.Session(pool, ssl.create_default_context()) + self._connected = True + + def neo_status(self, value): + """The status NeoPixel. + + :param value: The color to change the NeoPixel. + + """ + if self.neopix: + self.neopix.fill(value) + + @property + def is_connected(self): + return self._connected + + @property + def ip_address(self): + return wifi.radio.ipv4_address diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5aca98376a1f7e593ebd9cf41a808512c2135635 GIT binary patch literal 4414 zcmd^BX;4#F6n=SG-XmlONeGrD5E6J{RVh+e928U#MG!$jWvO+UsvWh`x&VqGNx*en zx=qox7Dqv{kPwo%fZC$dDwVpRtz{HzTkSs8QhG0)%Y=-3@Kt!4ag|JcIo?$-F|?bXVS9UDUyev>MVZQ(H8K4#;BQW-t2CPorj8^KJrMX}QK zp+e<;4ldpXz~=)2GxNy811&)gt-}Q*yVQpsxr@VMoA##{)$1~=bZ1MmjeFw?uT(`8 z^g=09<=zW%r%buwN%iHtuKSg|+r7HkT0PYN*_u9k1;^Ss-Z!RBfJ?Un4w(awqp2b3 z%+myoFis_lTlCrGx2z$0BQdh+7?!JK#9K9@Z!VrG zNj6gK5r(b4?YDOLw|DPRoN7bdP{(>GEG41YcN~4r_SUHU2hgVtUwZG@s%edC;k7Sn zC)RvEnlq~raE2mY2ko64^m1KQL}3riixh?#J{o)IT+K-RdHae2eRX91-+g!y`8^># z-zI0ir>P%Xon)!@xp-BK2bDYUB9k613NRrY6%lVjbFcQc*pRqiK~8xtkNPLxt}e?&QsTB}^!39t_%Qb)~Ukn0O%iC;zt z<&A-y;3h++)>c1br`5VFM~5(83!HKx$L+my8sW_c#@x*|*vB1yU)_dt3vH;2hqPWx zAl^6@?ipx&U7pf`a*>Yq6C85nb+B=Fnn+(id$W#WB^uHAcZVG`qg;rWB}ubvi(Y>D z$ei>REw$#xp0SHAd^|1hq&9HJ=jKK8^zTH~nk)G?yUcmTh9vUM6Y0LMw4(gYVY$D$ zGl&WY&H<)BbJ&3sYbKjx1j^=3-0Q#f^}(aP1?8^`&FUWMp|rmtpK)bLQ1Zo?^s4jqK=Lfg*9&geMGVQ z#^-*!V`fG@;H&{M9S8%+;|h&Qrxym0Ar>WT4BCVLR8cGXF=JmEYN(sNT(9vl+S|%g z8r7nXQ(95i^`=+XHo|){$vf2$?=`F$^&wFlYXyXg$B{a>$-Fp+V}+D;9k=~Xl~?C4 zAB-;RKXdUzBJE{V&d&%R>aEfFe;vxqI$0@hwVM}gFeQR@j}a>DDxR+n+-*6|_)k%% z*mSpDV|=5I9!&VC&9tD%fcVygWZV!iIo2qFtm#!*(s|@ZT33*Ad;+<|3^+yrp*;oH zBSYLV(H1zTU?2WjrCQoQW)Z>J2a=dTriuvezBmu16`tM2fm7Q@d4^iqII-xFpwHGI zn9CL}QE*1vdj2PX{PIuqOe5dracsciH6OlAZATvE8rj6ykqdIjal2 z0S0S~PwHb-5?OQ-tU-^KTG@XNrEVSvo|HIP?H;7ZhYeZkhSqh-{reE!5di;1zk$#Y zCe7rOnlzFYJ6Z#Hm$GoidKB=2HBCwm`BbZVeZY4ukmG%1uz7p2URs6c9j-Gjj^oQV zsdDb3@k2e`C$1I5ML5U0Qs0C1GAp^?!*`=|Nm(vWz3j*j*8ucum2;r0^-6Aca=Gv) zc%}&;!+_*S2tlnnJnz0EKeRmw-Y!@9ob!XQBwiv}^u9MkaXHvM=!<3YX;+2#5Cj5pp?FEK750S3BgeSDtaE^ zXUM@xoV6yBFKfzvY20V&Lr0yC + CircuitPython Reference Documentation + CircuitPython Support Forum + Discord Chat + Adafruit Learning System + Adafruit Blog + Adafruit Store + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/index.rst.license b/docs/index.rst.license new file mode 100644 index 0000000..11cd75d --- /dev/null +++ b/docs/index.rst.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + +SPDX-License-Identifier: MIT diff --git a/examples/magtag_simpletest.py b/examples/magtag_simpletest.py new file mode 100644 index 0000000..0d4a377 --- /dev/null +++ b/examples/magtag_simpletest.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f3c35ae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# +# SPDX-License-Identifier: Unlicense + +[tool.black] +target-version = ['py35'] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f01609c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +Adafruit-Blinka +adafruit-blinka-displayio +adafruit-circuitpython-bitmap-font +adafruit-circuitpython-display-text +adafruit-circuitpython-neopixel +adafruit-circuitpython-requests diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d601458 --- /dev/null +++ b/setup.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +"""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-circuitpython-magtag", + use_scm_version=True, + setup_requires=["setuptools_scm"], + description="Helper library for the Adafruit MagTag.", + long_description=long_description, + long_description_content_type="text/x-rst", + # The project's main homepage. + url="https://github.com/adafruit/Adafruit_CircuitPython_MagTag", + # Author details + author="Adafruit Industries", + author_email="circuitpython@adafruit.com", + install_requires=[ + "Adafruit-Blinka", + "adafruit-blinka-displayio", + "adafruit-circuitpython-bitmap-font", + "adafruit-circuitpython-display-text", + "adafruit-circuitpython-neopixel", + "adafruit-circuitpython-requests", + ], + # 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 magtag eInk ePaper EPD Portal", + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=["adafruit_magtag"], +) From 0c7b5850e062ffb73d4abf4a2f3a5af42abe062e Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Wed, 11 Nov 2020 11:38:36 -0800 Subject: [PATCH 2/9] Fix pre-commit hooks error + run black --- .pre-commit-config.yaml | 2 +- adafruit_magtag/epaper.py | 7 ++++++- adafruit_magtag/graphics.py | 18 ++++++------------ adafruit_magtag/magtag.py | 1 - adafruit_magtag/network.py | 10 ++++++++-- adafruit_magtag/wifi.py | 2 +- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fbda35c..aab5f1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/python/black - rev: latest + rev: stable hooks: - id: black - repo: https://github.com/fsfe/reuse-tool diff --git a/adafruit_magtag/epaper.py b/adafruit_magtag/epaper.py index e85ef1e..48820a9 100755 --- a/adafruit_magtag/epaper.py +++ b/adafruit_magtag/epaper.py @@ -39,6 +39,7 @@ __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" + class EPaper: """Class representing the EPaper Display. This is used to automatically initialize the display. @@ -67,7 +68,11 @@ def __init__( try: displayio.release_displays() display_bus = displayio.FourWire( - board.SPI(), command=board.EPD_DC, chip_select=board.EPD_CS, reset=board.EPD_RESET, baudrate=1000000 + board.SPI(), + command=board.EPD_DC, + chip_select=board.EPD_CS, + reset=board.EPD_RESET, + baudrate=1000000, ) sleep(1) if display_type == DISPLAY_TYPE_GRAYSCALE: diff --git a/adafruit_magtag/graphics.py b/adafruit_magtag/graphics.py index 7576c92..99029a8 100755 --- a/adafruit_magtag/graphics.py +++ b/adafruit_magtag/graphics.py @@ -51,12 +51,7 @@ class Graphics: # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements def __init__( - self, - *, - default_bg=None, - profile=MAGTAG_29_GRAYSCALE, - rotation=270, - debug=False + self, *, default_bg=None, profile=MAGTAG_29_GRAYSCALE, rotation=270, debug=False ): self._debug = debug @@ -115,7 +110,10 @@ def set_background(self, file_or_color, position=None): color_palette = displayio.Palette(1) color_palette[0] = file_or_color self._bg_sprite = displayio.TileGrid( - color_bitmap, pixel_shader=color_palette, x=position[0], y=position[1], + color_bitmap, + pixel_shader=color_palette, + x=position[0], + y=position[1], ) else: raise RuntimeError("Unknown type of background") @@ -125,9 +123,5 @@ def set_background(self, file_or_color, position=None): gc.collect() def qrcode(): - """ - - - - """ + """""" pass diff --git a/adafruit_magtag/magtag.py b/adafruit_magtag/magtag.py index 8e09f60..a92fbb9 100755 --- a/adafruit_magtag/magtag.py +++ b/adafruit_magtag/magtag.py @@ -341,7 +341,6 @@ def get_io_group(self, group_key): """ return self.network.get_io_group(group_key) - def fetch(self, refresh_url=None, timeout=10): """Fetch data from the url we initialized with, perfom any parsing, and display text or graphics. This function does pretty much everything diff --git a/adafruit_magtag/network.py b/adafruit_magtag/network.py index 0cc23ca..3db61f0 100755 --- a/adafruit_magtag/network.py +++ b/adafruit_magtag/network.py @@ -305,7 +305,7 @@ def connect(self): raise OSError(change_me) self._wifi.neo_status(STATUS_NO_CONNECTION) # red = not connected try: - self._wifi.connect(secrets['ssid'], secrets['password']) + self._wifi.connect(secrets["ssid"], secrets["password"]) self.requests = self._wifi.requests except RuntimeError as error: print("Could not connect to internet", error) @@ -429,7 +429,13 @@ def fetch(self, url, *, headers=None, timeout=10): return response def fetch_data( - self, url, *, headers=None, json_path=None, regexp_path=None, timeout=10, + self, + url, + *, + headers=None, + json_path=None, + regexp_path=None, + timeout=10, ): """Fetch data from the specified url and perfom any parsing""" json_out = None diff --git a/adafruit_magtag/wifi.py b/adafruit_magtag/wifi.py index 334cb56..c442c92 100755 --- a/adafruit_magtag/wifi.py +++ b/adafruit_magtag/wifi.py @@ -85,7 +85,7 @@ def neo_status(self, value): @property def is_connected(self): return self._connected - + @property def ip_address(self): return wifi.radio.ipv4_address From 14d7b126d3cfeb325c9fc32eaca14090d4f057ab Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Thu, 12 Nov 2020 14:03:58 -0800 Subject: [PATCH 3/9] Updated some more --- adafruit_magtag/epaper.py | 95 ------------------------------ adafruit_magtag/graphics.py | 69 ++++++++++++++-------- adafruit_magtag/magtag.py | 20 +------ adafruit_magtag/network.py | 8 +-- adafruit_magtag/peripherals.py | 103 +++++++++++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 145 deletions(-) delete mode 100755 adafruit_magtag/epaper.py create mode 100755 adafruit_magtag/peripherals.py diff --git a/adafruit_magtag/epaper.py b/adafruit_magtag/epaper.py deleted file mode 100755 index 48820a9..0000000 --- a/adafruit_magtag/epaper.py +++ /dev/null @@ -1,95 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries -# -# SPDX-License-Identifier: MIT -""" -`adafruit_magtag.epaper` -================================================================================ - -Helper Library for the Adafruit MagTag. - - -* Author(s): Melissa LeBlanc-Williams - -Implementation Notes --------------------- - -**Hardware:** - -* `Adafruit MagTag `_ - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - -""" - -import board -from time import sleep -import displayio -import adafruit_il0373 - -MAGTAG_29_GRAYSCALE = 1 - -DISPLAY_TYPE_MONO = 1 -DISPLAY_TYPE_TRICOLOR = 2 -DISPLAY_TYPE_GRAYSCALE = 3 - -__version__ = "0.0.0-auto.0" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" - - -class EPaper: - """Class representing the EPaper Display. This is used to automatically - initialize the display. - - :param int width: The width of the display in Pixels. Defaults to 64. - :param int height: The height of the display in Pixels. Defaults to 32. - :param int rotation: The degrees to rotate the display. Defaults to 270. - """ - - # pylint: disable=too-few-public-methods,too-many-branches - def __init__( - self, - profile=MAGTAG_29_GRAYSCALE, - rotation=270, - ): - display_type = None - self._refresh_time = 5 - - if profile == MAGTAG_29_GRAYSCALE: - width = 296 - height = 128 - display_type = DISPLAY_TYPE_GRAYSCALE - else: - raise ValueError("Unknown MagTag Profile") - - try: - displayio.release_displays() - display_bus = displayio.FourWire( - board.SPI(), - command=board.EPD_DC, - chip_select=board.EPD_CS, - reset=board.EPD_RESET, - baudrate=1000000, - ) - sleep(1) - if display_type == DISPLAY_TYPE_GRAYSCALE: - self.display = adafruit_il0373.IL0373( - display_bus, - width=width, - height=height, - rotation=rotation, - black_bits_inverted=False, - color_bits_inverted=False, - grayscale=True, - refresh_time=1, - seconds_per_frame=self._refresh_time, - ) - except ValueError: - raise RuntimeError("Failed to initialize ePaper Display") from ValueError - - @property - def refresh_time(self): - return self._refresh_time diff --git a/adafruit_magtag/graphics.py b/adafruit_magtag/graphics.py index 99029a8..ec2fdc2 100755 --- a/adafruit_magtag/graphics.py +++ b/adafruit_magtag/graphics.py @@ -27,8 +27,8 @@ import gc from time import sleep +import board import displayio -from adafruit_magtag.epaper import EPaper, MAGTAG_29_GRAYSCALE __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" @@ -39,27 +39,39 @@ class Graphics: :param default_bg: The path to your default background image file or a hex color. Defaults to 0x000000. - :param int width: The width of the display in Pixels. Defaults to 64. - :param int height: The height of the display in Pixels. Defaults to 32. - :param int bit_depth: The number of bits per color channel. Defaults to 2. - :param list alt_addr_pins: An alternate set of address pins to use. Defaults to None - :param string color_order: A string containing the letter "R", "G", and "B" in the - order you want. Defaults to "RGB" :param debug: Turn on debug print outs. Defaults to False. """ # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements - def __init__( - self, *, default_bg=None, profile=MAGTAG_29_GRAYSCALE, rotation=270, debug=False - ): + def __init__(self, *, default_bg=None, debug=False): self._debug = debug - epaper = EPaper( - profile=profile, - rotation=rotation, - ) - self.display = epaper.display + if not hasattr(board, "DISPLAY"): + import adafruit_il0373 + + displayio.release_displays() + display_bus = displayio.FourWire( + board.SPI(), + command=board.EPD_DC, + chip_select=board.EPD_CS, + reset=board.EPD_RESET, + baudrate=1000000, + ) + + self.display = adafruit_il0373.IL0373( + display_bus, + width=296, + height=128, + rotation=270, + black_bits_inverted=False, + color_bits_inverted=False, + grayscale=True, + refresh_time=1, + seconds_per_frame=5, + ) + else: + self.display = board.DISPLAY if self._debug: print("Init display") @@ -70,11 +82,11 @@ def __init__( self._bg_group = displayio.Group(max_size=1) self._bg_file = None self.splash.append(self._bg_group) + self.display.show(self.splash) # set the default background if default_bg is not None: self.set_background(default_bg) - self.display.show(self.splash) gc.collect() @@ -84,7 +96,7 @@ def set_background(self, file_or_color, position=None): :param file_or_color: The filename of the chosen background image, or a hex color. """ - print("Set background to ", file_or_color) + print("Set background to", file_or_color) while self._bg_group: self._bg_group.pop() @@ -97,6 +109,7 @@ def set_background(self, file_or_color, position=None): self._bg_file.close() if isinstance(file_or_color, str): # its a filenme: self._bg_file = open(file_or_color, "rb") + print("Displaying image file") background = displayio.OnDiskBitmap(self._bg_file) self._bg_sprite = displayio.TileGrid( background, @@ -106,22 +119,30 @@ def set_background(self, file_or_color, position=None): ) elif isinstance(file_or_color, int): # Make a background color fill + print("Displaying color") color_bitmap = displayio.Bitmap(self.display.width, self.display.height, 1) color_palette = displayio.Palette(1) color_palette[0] = file_or_color self._bg_sprite = displayio.TileGrid( - color_bitmap, - pixel_shader=color_palette, - x=position[0], - y=position[1], + color_bitmap, pixel_shader=color_palette, x=position[0], y=position[1], ) else: raise RuntimeError("Unknown type of background") self._bg_group.append(self._bg_sprite) self.display.refresh() - sleep(self.epaper.refresh_time) + sleep(5) gc.collect() - def qrcode(): - """""" + def qrcode(self): pass + + +""" +f = open("/blinka_strength_tarot_5.bmp", "rb") +pic = displayio.OnDiskBitmap(f) +t = displayio.TileGrid(pic, pixel_shader=displayio.ColorConverter()) +g.append(t) + +display.refresh() +print("refreshed") +""" diff --git a/adafruit_magtag/magtag.py b/adafruit_magtag/magtag.py index a92fbb9..4e8c9e0 100755 --- a/adafruit_magtag/magtag.py +++ b/adafruit_magtag/magtag.py @@ -125,9 +125,7 @@ def __init__( self._text_wrap = [] self._text_maxlen = [] self._text_transform = [] - self._text_scrolling = [] self._text_scale = [] - self._scrolling_index = None self._text_font = [] self._text_line_spacing = [] @@ -136,14 +134,13 @@ def __init__( # pylint: disable=too-many-arguments def add_text( self, - text_position=None, + text_position=(0, 0), text_font=terminalio.FONT, text_color=0x808080, text_wrap=False, text_maxlen=0, text_transform=None, text_scale=1, - scrolling=False, line_spacing=1.25, ): """ @@ -160,10 +157,6 @@ def add_text( :param text_maxlen: The max length of the text for text wrapping. Defaults to 0. :param text_transform: A function that will be called on the text before display :param int text_scale: The factor to scale the default size of the text by - :param bool scrolling: If true, text is placed offscreen and the scroll() function is used - to scroll text on a pixel-by-pixel basis. Multiple text labels with - the scrolling set to True will be cycled through. - """ if text_font is terminalio.FONT: self._text_font.append(text_font) @@ -178,13 +171,6 @@ def add_text( if not isinstance(text_scale, (int, float)) or text_scale < 1: text_scale = 1 text_scale = round(text_scale) - if scrolling: - if text_position is None: - # Center text if position not specified - text_position = (self.display.width, self.display.height // 2 - 1) - else: - text_position = (self.display.width, text_position[1]) - gc.collect() if self._debug: @@ -196,12 +182,8 @@ def add_text( self._text_maxlen.append(text_maxlen) self._text_transform.append(text_transform) self._text_scale.append(text_scale) - self._text_scrolling.append(scrolling) self._text_line_spacing.append(line_spacing) - if scrolling and self._scrolling_index is None: # Not initialized yet - self._scrolling_index = self._get_next_scrollable_text_index() - # pylint: enable=too-many-arguments @staticmethod diff --git a/adafruit_magtag/network.py b/adafruit_magtag/network.py index 3db61f0..3496d02 100755 --- a/adafruit_magtag/network.py +++ b/adafruit_magtag/network.py @@ -429,13 +429,7 @@ def fetch(self, url, *, headers=None, timeout=10): return response def fetch_data( - self, - url, - *, - headers=None, - json_path=None, - regexp_path=None, - timeout=10, + self, url, *, headers=None, json_path=None, regexp_path=None, timeout=10, ): """Fetch data from the specified url and perfom any parsing""" json_out = None diff --git a/adafruit_magtag/peripherals.py b/adafruit_magtag/peripherals.py new file mode 100755 index 0000000..693dce7 --- /dev/null +++ b/adafruit_magtag/peripherals.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +`adafruit_magtag.peripherals` +================================================================================ + +Helper Library for the Adafruit MagTag. + + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit MagTag `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +import board +from digitalio import DigitalInOut, Direction, Pull +from analogio import AnalogIn +import neopixel +import audioio +import audiocore +import simpleio + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" + + +class Graphics: + """Peripherals Helper Class for the MagTag Library + + :param default_bg: The path to your default background image file or a hex color. + Defaults to 0x000000. + :param debug: Turn on debug print outs. Defaults to False. + + """ + + # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements + def __init__(self): + # Neopixels + self.neopixels = neopixel.NeoPixel(board.NEOPIXEL, 4, brightness=0.3) + pixel_power = DigitalInOut(board.NEOPIXEL_POWER) + pixel_power.direction = Direction.OUTPUT + pixel_power.value = False + + # Battery Voltage + self._batt_monitor = AnalogIn(board.BATTERY) + + # Speaker Enable + self._speaker_enable = DigitalInOut(board.SPEAKER_ENABLE) + self._speaker_enable.direction = Direction.OUTPUT + self._speaker_enable.value = False + + if hasattr(board, "SPEAKER"): + self.audio = audioio.AudioOut(board.SPEAKER) + else: + raise AttributeError("Board does not have a builtin speaker!") + + # Buttons + self.buttons = [] + for p in (board.BUTTON_A, board.BUTTON_B, board.BUTTON_C, board.BUTTON_D): + switch = DigitalInOut(p) + switch.direction = Direction.INPUT + switch.pull = Pull.UP + self.buttons.append(switch) + + def play_tone(self, frequency, duration): + self._speaker_enable.value = True + simpleio.tone(board.SPEAKER, frequency, duration) + self._speaker_enable.value = False + + def play_wav_file(self, file_name, wait_to_finish=True): + """Play a wav file. + + :param str file_name: The name of the wav file to play on the speaker. + + """ + wavfile = open(file_name, "rb") + wavedata = audiocore.WaveFile(wavfile) + self._speaker_enable.value = True + self.audio.play(wavedata) + if not wait_to_finish: + return + while self.audio.playing: + pass + wavfile.close() + self._speaker_enable.value = False + + @property + def battery(self): + """Return the voltage of the battery""" + return (self._batt_monitor.value / 65535.0) * 2.6 * 2 From a1df065cd12f4f95d465adc9e8a56d9f06e6b264 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 13 Nov 2020 09:56:01 -0800 Subject: [PATCH 4/9] Add QR, Improvements, Linted --- adafruit_magtag/graphics.py | 83 ++++++++++++++++----- adafruit_magtag/magtag.py | 46 +++++------- adafruit_magtag/network.py | 58 +++++++++----- adafruit_magtag/peripherals.py | 57 ++++++-------- adafruit_magtag/{wifi.py => wifi_module.py} | 35 +++++---- examples/bitcoin_peripheral_demo.py | 77 +++++++++++++++++++ 6 files changed, 243 insertions(+), 113 deletions(-) rename adafruit_magtag/{wifi.py => wifi_module.py} (72%) create mode 100644 examples/bitcoin_peripheral_demo.py diff --git a/adafruit_magtag/graphics.py b/adafruit_magtag/graphics.py index ec2fdc2..a3229ea 100755 --- a/adafruit_magtag/graphics.py +++ b/adafruit_magtag/graphics.py @@ -39,16 +39,18 @@ class Graphics: :param default_bg: The path to your default background image file or a hex color. Defaults to 0x000000. + :param bool auto_refresh: Automatically refresh the eInk after writing to displayio. + Defaults to True. :param debug: Turn on debug print outs. Defaults to False. """ # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements - def __init__(self, *, default_bg=None, debug=False): + def __init__(self, *, default_bg=None, auto_refresh=True, debug=False): self._debug = debug if not hasattr(board, "DISPLAY"): - import adafruit_il0373 + import adafruit_il0373 # pylint: disable=import-outside-toplevel displayio.release_displays() display_bus = displayio.FourWire( @@ -73,10 +75,12 @@ def __init__(self, *, default_bg=None, debug=False): else: self.display = board.DISPLAY + self.auto_refresh = auto_refresh + if self._debug: print("Init display") self.splash = displayio.Group(max_size=15) - + self._qr_group = None if self._debug: print("Init background") self._bg_group = displayio.Group(max_size=1) @@ -94,6 +98,7 @@ def set_background(self, file_or_color, position=None): """The background image to a bitmap file. :param file_or_color: The filename of the chosen background image, or a hex color. + :param tuple position: Optional x and y coordinates to place the background at. """ print("Set background to", file_or_color) @@ -124,25 +129,69 @@ def set_background(self, file_or_color, position=None): color_palette = displayio.Palette(1) color_palette[0] = file_or_color self._bg_sprite = displayio.TileGrid( - color_bitmap, pixel_shader=color_palette, x=position[0], y=position[1], + color_bitmap, + pixel_shader=color_palette, + x=position[0], + y=position[1], ) else: raise RuntimeError("Unknown type of background") self._bg_group.append(self._bg_sprite) - self.display.refresh() - sleep(5) + if self.auto_refresh: + self.display.refresh() + sleep(5) gc.collect() - def qrcode(self): - pass + def qrcode( + self, qr_data, *, qr_size=1, x=0, y=0, qr_color=0x000000 + ): # pylint: disable=invalid-name + """Display a QR code on the eInk + :param qr_data: The data for the QR code. + :param int qr_size: The scale of the QR code. + :param x: The x position of upper left corner of the QR code on the display. + :param y: The y position of upper left corner of the QR code on the display. -""" -f = open("/blinka_strength_tarot_5.bmp", "rb") -pic = displayio.OnDiskBitmap(f) -t = displayio.TileGrid(pic, pixel_shader=displayio.ColorConverter()) -g.append(t) - -display.refresh() -print("refreshed") -""" + """ + import adafruit_miniqr # pylint: disable=import-outside-toplevel + + # generate the QR code + qrcode = adafruit_miniqr.QRCode() + qrcode.add_data(qr_data) + qrcode.make() + + # monochrome (2 color) palette + palette = displayio.Palette(2) + palette[0] = 0xFFFFFF + palette[1] = qr_color + + # pylint: disable=invalid-name + # bitmap the size of the matrix, plus border, monochrome (2 colors) + qr_bitmap = displayio.Bitmap( + qrcode.matrix.width + 2, qrcode.matrix.height + 2, 2 + ) + for i in range(qr_bitmap.width * qr_bitmap.height): + qr_bitmap[i] = 0 + + # transcribe QR code into bitmap + for xx in range(qrcode.matrix.width): + for yy in range(qrcode.matrix.height): + qr_bitmap[xx + 1, yy + 1] = 1 if qrcode.matrix[xx, yy] else 0 + + # display the QR code + qr_sprite = displayio.TileGrid(qr_bitmap, pixel_shader=palette) + if self._qr_group: + try: + self._qr_group.pop() + except IndexError: # later test if empty + pass + else: + self._qr_group = displayio.Group() + self.splash.append(self._qr_group) + self._qr_group.scale = qr_size + self._qr_group.x = x + self._qr_group.y = y + self._qr_group.append(qr_sprite) + if self.auto_refresh: + self.display.refresh() + sleep(5) diff --git a/adafruit_magtag/magtag.py b/adafruit_magtag/magtag.py index 4e8c9e0..bdb0ddb 100755 --- a/adafruit_magtag/magtag.py +++ b/adafruit_magtag/magtag.py @@ -26,12 +26,12 @@ """ import gc -from time import sleep import terminalio from adafruit_bitmap_font import bitmap_font from adafruit_display_text.label import Label from adafruit_magtag.network import Network from adafruit_magtag.graphics import Graphics +from adafruit_magtag.peripherals import Peripherals __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" @@ -53,13 +53,6 @@ class MagTag: NeoPixel. Defaults to ``None``, not the status LED :param json_transform: A function or a list of functions to call with the parsed JSON. Changes and additions are permitted for the ``dict`` object. - :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used - before calling the pyportal class. Defaults to ``None``. - :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. - :param int bit_depth: The number of bits per color channel. Defaults to 2. - :param list alt_addr_pins: An alternate set of address pins to use. Defaults to None - :param string color_order: A string containing the letter "R", "G", and "B" in the - order you want. Defaults to "RGB" :param debug: Turn on debug print outs. Defaults to False. """ @@ -72,35 +65,21 @@ def __init__( headers=None, json_path=None, regexp_path=None, - default_bg=0x000000, + default_bg=None, status_neopixel=None, json_transform=None, - esp=None, - external_spi=None, - bit_depth=2, - alt_addr_pins=None, - color_order="RGB", debug=False, - width=64, - height=32, ): self._debug = debug self.graphics = Graphics( default_bg=default_bg, - bit_depth=bit_depth, - width=width, - height=height, - alt_addr_pins=alt_addr_pins, - color_order=color_order, debug=debug, ) self.display = self.graphics.display self.network = Network( status_neopixel=status_neopixel, - esp=esp, - external_spi=external_spi, extract_values=False, debug=debug, ) @@ -115,6 +94,8 @@ def __init__( self.splash = self.graphics.splash + self.peripherals = Peripherals() + # Add any JSON translators if json_transform: self.network.add_json_transform(json_transform) @@ -136,7 +117,7 @@ def add_text( self, text_position=(0, 0), text_font=terminalio.FONT, - text_color=0x808080, + text_color=0x000000, text_wrap=False, text_maxlen=0, text_transform=None, @@ -241,7 +222,7 @@ def set_text_color(self, color, index=0): self._text_color[index] = color self._text[index].color = color - def set_text(self, val, index=0): + def set_text(self, val, index=0, auto_refresh=True): """Display text, with indexing into our list of text boxes. :param str val: The text to be displayed @@ -282,6 +263,8 @@ def set_text(self, val, index=0): del self.splash[index_in_splash] elif self._text[index] is not None: self.splash.append(self._text[index]) + if auto_refresh: + self.refresh() def get_local_time(self, location=None): """Accessor function for get_local_time()""" @@ -323,10 +306,20 @@ def get_io_group(self, group_key): """ return self.network.get_io_group(group_key) + def refresh(self): + """ + Refresh the display + """ + self.graphics.display.refresh() + def fetch(self, refresh_url=None, timeout=10): """Fetch data from the url we initialized with, perfom any parsing, and display text or graphics. This function does pretty much everything Optionally update the URL + + :param str refresh_url: The overriding URL to fetch from. Defaults to ``None``. + :param int timeout: The timeout period in seconds. + """ if refresh_url: self._url = refresh_url @@ -359,7 +352,8 @@ def fetch(self, refresh_url=None, timeout=10): print("Wrapping text") lines = self.wrap_nicely(string, self._text_wrap[i]) string = "\n".join(lines) - self.set_text(string, index=i) + self.set_text(string, index=i, auto_refresh=False) + self.refresh() if len(values) == 1: return values[0] return values diff --git a/adafruit_magtag/network.py b/adafruit_magtag/network.py index 3496d02..ba98c4d 100755 --- a/adafruit_magtag/network.py +++ b/adafruit_magtag/network.py @@ -32,7 +32,7 @@ from adafruit_io.adafruit_io import IO_HTTP, AdafruitIO_RequestError import supervisor import rtc -from adafruit_magtag.wifi import WiFi +from adafruit_magtag.wifi_module import WiFi from adafruit_magtag.fakerequests import Fake_Requests @@ -80,13 +80,10 @@ class HttpError(Exception): class Network: - """Class representing the Adafruit RGB Matrix Portal. + """Class representing the Adafruit MagTag. :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board NeoPixel. Defaults to ``None``, not the status LED - :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used - before calling the pyportal class. Defaults to ``None``. - :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. :param bool extract_values: If true, single-length fetched values are automatically extracted from lists and tuples. Defaults to ``True``. :param debug: Turn on debug print outs. Defaults to False. @@ -98,14 +95,10 @@ def __init__( self, *, status_neopixel=None, - esp=None, - external_spi=None, extract_values=True, debug=False, ): - self._wifi = WiFi( - status_neopixel=status_neopixel, esp=esp, external_spi=external_spi - ) + self._wifi = WiFi(status_neopixel=status_neopixel) self._debug = debug self.json_transform = [] self._extract_values = extract_values @@ -161,7 +154,10 @@ def add_json_transform(self, json_transform): def get_local_time(self, location=None): # pylint: disable=line-too-long - """Fetch and "set" the local time of this microcontroller to the local time at the location, using an internet time API. + """ + NOTE: Do not use yet. This is currently not working. + + Fetch and "set" the local time of this microcontroller to the local time at the location, using an internet time API. :param str location: Your city and country, e.g. ``"New York, US"``. @@ -328,6 +324,8 @@ def _get_io_client(self): def push_to_io(self, feed_key, data): """Push data to an adafruit.io feed + NOTE: Do not use yet. This is currently not working. + :param str feed_key: Name of feed key to push data to. :param data: data to send to feed @@ -360,6 +358,8 @@ def push_to_io(self, feed_key, data): def get_io_feed(self, feed_key, detailed=False): """Return the Adafruit IO Feed that matches the feed key + NOTE: Do not use yet. This is currently not working. + :param str feed_key: Name of feed key to match. :param bool detailed: Whether to return additional detailed information @@ -377,6 +377,8 @@ def get_io_feed(self, feed_key, detailed=False): def get_io_group(self, group_key): """Return the Adafruit IO Group that matches the group key + NOTE: Do not use yet. This is currently not working. + :param str group_key: Name of group key to match. """ @@ -393,6 +395,8 @@ def get_io_group(self, group_key): def get_io_data(self, feed_key): """Return all values from Adafruit IO Feed Data that matches the feed key + NOTE: Do not use yet. This is currently not working. + :param str feed_key: Name of feed key to receive data from. """ @@ -407,7 +411,13 @@ def get_io_data(self, feed_key): break def fetch(self, url, *, headers=None, timeout=10): - """Fetch data from the specified url and return a response object""" + """Fetch data from the specified url and return a response object + + :param str url: The URL to fetch from. + :param list headers: Extra headers to include in the request. + :param int timeout: The timeout period in seconds. + + """ gc.collect() if self._debug: print("Free mem: ", gc.mem_free()) # pylint: disable=no-member @@ -429,9 +439,24 @@ def fetch(self, url, *, headers=None, timeout=10): return response def fetch_data( - self, url, *, headers=None, json_path=None, regexp_path=None, timeout=10, + self, + url, + *, + headers=None, + json_path=None, + regexp_path=None, + timeout=10, ): - """Fetch data from the specified url and perfom any parsing""" + """Fetch data from the specified url and perfom any parsing + + :param str url: The URL to fetch from. + :param list headers: Extra headers to include in the request. + :param json_path: The path to drill down into the JSON data. + :param regexp_path: The path formatted as a regular expression to drill down + into the JSON data. + :param int timeout: The timeout period in seconds. + + """ json_out = None values = [] content_type = CONTENT_TEXT @@ -524,8 +549,3 @@ def fetch_data( return values[0] return values - - @property - def ip_address(self): - """Return the IP Address nicely formatted""" - return self._wifi.ip_address diff --git a/adafruit_magtag/peripherals.py b/adafruit_magtag/peripherals.py index 693dce7..333eac5 100755 --- a/adafruit_magtag/peripherals.py +++ b/adafruit_magtag/peripherals.py @@ -29,30 +29,22 @@ from digitalio import DigitalInOut, Direction, Pull from analogio import AnalogIn import neopixel -import audioio -import audiocore import simpleio __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" -class Graphics: - """Peripherals Helper Class for the MagTag Library - - :param default_bg: The path to your default background image file or a hex color. - Defaults to 0x000000. - :param debug: Turn on debug print outs. Defaults to False. - - """ +class Peripherals: + """Peripherals Helper Class for the MagTag Library""" # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements def __init__(self): # Neopixels self.neopixels = neopixel.NeoPixel(board.NEOPIXEL, 4, brightness=0.3) - pixel_power = DigitalInOut(board.NEOPIXEL_POWER) - pixel_power.direction = Direction.OUTPUT - pixel_power.value = False + self._neopixel_disable = DigitalInOut(board.NEOPIXEL_POWER) + self._neopixel_disable.direction = Direction.OUTPUT + self._neopixel_disable.value = False # Battery Voltage self._batt_monitor = AnalogIn(board.BATTERY) @@ -62,42 +54,35 @@ def __init__(self): self._speaker_enable.direction = Direction.OUTPUT self._speaker_enable.value = False - if hasattr(board, "SPEAKER"): - self.audio = audioio.AudioOut(board.SPEAKER) - else: - raise AttributeError("Board does not have a builtin speaker!") - # Buttons self.buttons = [] - for p in (board.BUTTON_A, board.BUTTON_B, board.BUTTON_C, board.BUTTON_D): - switch = DigitalInOut(p) + for pin in (board.BUTTON_A, board.BUTTON_B, board.BUTTON_C, board.BUTTON_D): + switch = DigitalInOut(pin) switch.direction = Direction.INPUT switch.pull = Pull.UP self.buttons.append(switch) def play_tone(self, frequency, duration): - self._speaker_enable.value = True - simpleio.tone(board.SPEAKER, frequency, duration) - self._speaker_enable.value = False - - def play_wav_file(self, file_name, wait_to_finish=True): - """Play a wav file. - - :param str file_name: The name of the wav file to play on the speaker. + """Automatically Enable/Disable the speaker and play + a tone at the specified frequency for the specified duration """ - wavfile = open(file_name, "rb") - wavedata = audiocore.WaveFile(wavfile) self._speaker_enable.value = True - self.audio.play(wavedata) - if not wait_to_finish: - return - while self.audio.playing: - pass - wavfile.close() + simpleio.tone(board.SPEAKER, frequency, duration) self._speaker_enable.value = False @property def battery(self): """Return the voltage of the battery""" return (self._batt_monitor.value / 65535.0) * 2.6 * 2 + + @property + def neopixel_disable(self): + """ + Enable or disable the neopixels for power savings + """ + return self._neopixel_disable.value + + @neopixel_disable.setter + def neopixel_disable(self, value): + self._neopixel_disable.value = value diff --git a/adafruit_magtag/wifi.py b/adafruit_magtag/wifi_module.py similarity index 72% rename from adafruit_magtag/wifi.py rename to adafruit_magtag/wifi_module.py index c442c92..32259fc 100755 --- a/adafruit_magtag/wifi.py +++ b/adafruit_magtag/wifi_module.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: MIT """ -`adafruit_magtag.wifi` +`adafruit_magtag.wifi_module` ================================================================================ Helper Library for the Adafruit MagTag. @@ -16,9 +16,7 @@ **Hardware:** -* `Adafruit Metro M4 Express AirLift `_ -* `Adafruit RGB Matrix Shield `_ -* `64x32 RGB LED Matrix `_ +* `Adafruit MagTag `_ **Software and Dependencies:** @@ -28,33 +26,25 @@ """ import gc -import board -import busio -from digitalio import DigitalInOut +import ssl import neopixel -import ipaddress import wifi import socketpool import adafruit_requests -import ssl -import espidf __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MagTag.git" class WiFi: - """Class representing the ESP. + """Class representing the WiFi portion of the ESP32-S2. :param status_neopixel: The pin for the status NeoPixel. Use ``board.NEOPIXEL`` for the on-board NeoPixel. Defaults to ``None``, not the status LED - :param esp: A passed ESP32 object, Can be used in cases where the ESP32 chip needs to be used - before calling the pyportal class. Defaults to ``None``. - :param busio.SPI external_spi: A previously declared spi object. Defaults to ``None``. """ - def __init__(self, *, status_neopixel=None, esp=None, external_spi=None): + def __init__(self, *, status_neopixel=None): if status_neopixel: self.neopix = neopixel.NeoPixel(status_neopixel, 1, brightness=0.2) @@ -67,6 +57,13 @@ def __init__(self, *, status_neopixel=None, esp=None, external_spi=None): gc.collect() def connect(self, ssid, password): + """ + Connect to the WiFi Network using the information provided + + :param ssid: The WiFi name + :param password: The WiFi password + + """ print(ssid, password) wifi.radio.connect(ssid, password) pool = socketpool.SocketPool(wifi.radio) @@ -84,8 +81,16 @@ def neo_status(self, value): @property def is_connected(self): + """ + Return whether we have already connected since reconnections are handled automatically. + + """ return self._connected @property def ip_address(self): + """ + Return the IP Version 4 Address + + """ return wifi.radio.ipv4_address diff --git a/examples/bitcoin_peripheral_demo.py b/examples/bitcoin_peripheral_demo.py new file mode 100644 index 0000000..affb0a1 --- /dev/null +++ b/examples/bitcoin_peripheral_demo.py @@ -0,0 +1,77 @@ +import time +import board +import displayio +import terminalio +from adafruit_display_text import label +from adafruit_magtag.magtag import MagTag + +i2c = board.I2C() + +# You can display in 'GBP', 'EUR' or 'USD' +CURRENCY = "USD" +# Set up where we'll be fetching data from +DATA_SOURCE = "https://api.coindesk.com/v1/bpi/currentprice.json" +DATA_LOCATION = ["bpi", CURRENCY, "rate_float"] + + +def text_transform(val): + if CURRENCY == "USD": + return "$%d" % val + if CURRENCY == "EUR": + return "‎€%d" % val + if CURRENCY == "GBP": + return "£%d" % val + return "%d" % val + + +magtag = MagTag( + default_bg="/bitcoin_grayscale.bmp", url=DATA_SOURCE, json_path=DATA_LOCATION, +) + +magtag.network.connect() + +magtag.add_text( + text_font=terminalio.FONT, + text_position=( + (magtag.graphics.display.width // 2) - 1, + (magtag.graphics.display.height // 2) - 1, + ), + text_scale=3, +) + +magtag.preload_font(b"$012345789") # preload numbers +magtag.preload_font((0x00A3, 0x20AC)) # preload gbp/euro symbol + +buttons = magtag.peripherals.buttons +button_colors = ((255, 0, 0), (255, 150, 0), (0, 255, 255), (180, 0, 255)) +button_tones = (1047, 1318, 1568, 2093) +timestamp = time.monotonic() + +while True: + for i, b in enumerate(buttons): + if not b.value: + print("Button %c pressed" % chr((ord("A") + i))) + magtag.peripherals.neopixel_disable = False + magtag.peripherals.neopixels.fill(button_colors[i]) + magtag.peripherals.play_tone(button_tones[i], 0.25) + break + else: + magtag.peripherals.neopixel_disable = True + + if (time.monotonic() - timestamp) > 5: # once a second... + print("Battery voltage? ", magtag.peripherals.battery) + while not i2c.try_lock(): + pass + print( + "I2C addresses found:", + [hex(device_address) for device_address in i2c.scan()], + ) + i2c.unlock() + try: + value = magtag.fetch() + print("Response is", value) + except (ValueError, RuntimeError) as e: + print("Some error occured, retrying! -", e) + timestamp = time.monotonic() + + time.sleep(0.01) From bf190cc5fb4e2a15998560c5484bc0860b70502f Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 13 Nov 2020 10:44:08 -0800 Subject: [PATCH 5/9] Updated docs, improved examples --- README.rst | 69 +++++++++++++++----------- adafruit_magtag/graphics.py | 3 +- adafruit_magtag/magtag.py | 3 +- docs/api.rst | 19 +++++-- docs/conf.py | 2 +- docs/examples.rst | 7 +++ docs/index.rst | 6 +-- examples/bitcoin_demo.py | 45 +++++++++++++++++ examples/bitcoin_peripheral_demo.py | 77 ----------------------------- examples/magtag_simpletest.py | 33 +++++++++++++ setup.py | 63 ----------------------- setup.py.disabled | 9 ++++ 12 files changed, 154 insertions(+), 182 deletions(-) create mode 100644 examples/bitcoin_demo.py delete mode 100644 examples/bitcoin_peripheral_demo.py delete mode 100644 setup.py create mode 100644 setup.py.disabled diff --git a/README.rst b/README.rst index ef967aa..509864b 100644 --- a/README.rst +++ b/README.rst @@ -30,40 +30,49 @@ Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading `the Adafruit library and driver bundle `_. -Installing from PyPI -===================== -.. note:: This library is not available on PyPI yet. Install documentation is included - as a standard element. Stay tuned for PyPI availability! - -.. todo:: Remove the above note if PyPI version is/will be available at time of release. - If the library is not planned for PyPI, remove the entire 'Installing from PyPI' section. - -On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from -PyPI `_. To install for current user: - -.. code-block:: shell - - pip3 install adafruit-circuitpython-magtag - -To install system-wide (this may be required in some cases): - -.. code-block:: shell - - sudo pip3 install adafruit-circuitpython-magtag - -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-circuitpython-magtag Usage Example ============= -.. todo:: Add a quick, simple example. It and other examples should live in the examples folder and be included in docs/examples.rst. +.. code:: python + + # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries + # + # SPDX-License-Identifier: Unlicense + import time + import terminalio + from adafruit_magtag.magtag import MagTag + + magtag = MagTag() + + magtag.add_text( + text_font=terminalio.FONT, + text_position=( + 50, + (magtag.graphics.display.height // 2) - 1, + ), + text_scale=3, + ) + + magtag.set_text("Hello World") + + buttons = magtag.peripherals.buttons + button_colors = ((255, 0, 0), (255, 150, 0), (0, 255, 255), (180, 0, 255)) + button_tones = (1047, 1318, 1568, 2093) + timestamp = time.monotonic() + + while True: + for i, b in enumerate(buttons): + if not b.value: + print("Button %c pressed" % chr((ord("A") + i))) + magtag.peripherals.neopixel_disable = False + magtag.peripherals.neopixels.fill(button_colors[i]) + magtag.peripherals.play_tone(button_tones[i], 0.25) + break + else: + magtag.peripherals.neopixel_disable = True + time.sleep(0.01) + Contributing ============ diff --git a/adafruit_magtag/graphics.py b/adafruit_magtag/graphics.py index a3229ea..0781794 100755 --- a/adafruit_magtag/graphics.py +++ b/adafruit_magtag/graphics.py @@ -46,7 +46,7 @@ class Graphics: """ # pylint: disable=too-many-instance-attributes, too-many-locals, too-many-branches, too-many-statements - def __init__(self, *, default_bg=None, auto_refresh=True, debug=False): + def __init__(self, *, default_bg=0xFFFFFF, auto_refresh=True, debug=False): self._debug = debug if not hasattr(board, "DISPLAY"): @@ -139,7 +139,6 @@ def set_background(self, file_or_color, position=None): self._bg_group.append(self._bg_sprite) if self.auto_refresh: self.display.refresh() - sleep(5) gc.collect() def qrcode( diff --git a/adafruit_magtag/magtag.py b/adafruit_magtag/magtag.py index bdb0ddb..2c1e846 100755 --- a/adafruit_magtag/magtag.py +++ b/adafruit_magtag/magtag.py @@ -65,7 +65,7 @@ def __init__( headers=None, json_path=None, regexp_path=None, - default_bg=None, + default_bg=0xFFFFFF, status_neopixel=None, json_transform=None, debug=False, @@ -74,6 +74,7 @@ def __init__( self._debug = debug self.graphics = Graphics( default_bg=default_bg, + auto_refresh=False, debug=debug, ) self.display = self.graphics.display diff --git a/docs/api.rst b/docs/api.rst index 025f34c..5bfbe85 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,8 +1,19 @@ -.. 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:: adafruit_magtag.fakerequests + :members: + +.. automodule:: adafruit_magtag.graphics + :members: + +.. automodule:: adafruit_magtag.magtag + :members: + +.. automodule:: adafruit_magtag.network + :members: + +.. automodule:: adafruit_magtag.wifi_module + :members: -.. automodule:: adafruit_magtag +.. automodule:: adafruit_magtag.peripherals :members: diff --git a/docs/conf.py b/docs/conf.py index d44b31e..5cea527 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ # 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"] +autodoc_mock_imports = ["supervisor", "rtc", "ssl", "wifi", "socketpool", "secrets"] intersphinx_mapping = { diff --git a/docs/examples.rst b/docs/examples.rst index aff5f61..08badc7 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -6,3 +6,10 @@ Ensure your device works with this simple test. .. literalinclude:: ../examples/magtag_simpletest.py :caption: examples/magtag_simpletest.py :linenos: + +Other Demos +------------ + +.. literalinclude:: ../examples/bitcoin_demo.py + :caption: examples/bitcoin_demo.py + :linenos: diff --git a/docs/index.rst b/docs/index.rst index 2e0ce06..dbe0dba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,14 +23,12 @@ Table of Contents .. toctree:: :caption: Tutorials -.. todo:: Add any Learn guide links here. If there are none, then simply delete this todo and leave - the toctree above for use later. + * Adafruit MagTag .. toctree:: :caption: Related Products -.. todo:: Add any product links here. If there are none, then simply delete this todo and leave - the toctree above for use later. + * Adafruit MagTag .. toctree:: :caption: Other Links diff --git a/examples/bitcoin_demo.py b/examples/bitcoin_demo.py new file mode 100644 index 0000000..116b3d8 --- /dev/null +++ b/examples/bitcoin_demo.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense +import time +import terminalio +from adafruit_magtag.magtag import MagTag + +# Set up where we'll be fetching data from +DATA_SOURCE = "https://api.coindesk.com/v1/bpi/currentprice.json" +DATA_LOCATION = ["bpi", "USD", "rate_float"] + +def text_transform(val): + return "Bitcoin: $%d" % val + +magtag = MagTag( + url=DATA_SOURCE, json_path=DATA_LOCATION, +) + +magtag.network.connect() + +magtag.add_text( + text_font=terminalio.FONT, + text_position=( + 10, + (magtag.graphics.display.height // 2) - 1, + ), + text_scale=3, + text_transform=text_transform, +) + +magtag.preload_font(b"$012345789") # preload numbers +magtag.preload_font((0x00A3, 0x20AC)) # preload gbp/euro symbol + +timestamp = time.monotonic() + +while True: + if (time.monotonic() - timestamp) > 5: # once a second... + try: + value = magtag.fetch() + print("Response is", value) + except (ValueError, RuntimeError) as e: + print("Some error occured, retrying! -", e) + timestamp = time.monotonic() + + time.sleep(0.01) diff --git a/examples/bitcoin_peripheral_demo.py b/examples/bitcoin_peripheral_demo.py deleted file mode 100644 index affb0a1..0000000 --- a/examples/bitcoin_peripheral_demo.py +++ /dev/null @@ -1,77 +0,0 @@ -import time -import board -import displayio -import terminalio -from adafruit_display_text import label -from adafruit_magtag.magtag import MagTag - -i2c = board.I2C() - -# You can display in 'GBP', 'EUR' or 'USD' -CURRENCY = "USD" -# Set up where we'll be fetching data from -DATA_SOURCE = "https://api.coindesk.com/v1/bpi/currentprice.json" -DATA_LOCATION = ["bpi", CURRENCY, "rate_float"] - - -def text_transform(val): - if CURRENCY == "USD": - return "$%d" % val - if CURRENCY == "EUR": - return "‎€%d" % val - if CURRENCY == "GBP": - return "£%d" % val - return "%d" % val - - -magtag = MagTag( - default_bg="/bitcoin_grayscale.bmp", url=DATA_SOURCE, json_path=DATA_LOCATION, -) - -magtag.network.connect() - -magtag.add_text( - text_font=terminalio.FONT, - text_position=( - (magtag.graphics.display.width // 2) - 1, - (magtag.graphics.display.height // 2) - 1, - ), - text_scale=3, -) - -magtag.preload_font(b"$012345789") # preload numbers -magtag.preload_font((0x00A3, 0x20AC)) # preload gbp/euro symbol - -buttons = magtag.peripherals.buttons -button_colors = ((255, 0, 0), (255, 150, 0), (0, 255, 255), (180, 0, 255)) -button_tones = (1047, 1318, 1568, 2093) -timestamp = time.monotonic() - -while True: - for i, b in enumerate(buttons): - if not b.value: - print("Button %c pressed" % chr((ord("A") + i))) - magtag.peripherals.neopixel_disable = False - magtag.peripherals.neopixels.fill(button_colors[i]) - magtag.peripherals.play_tone(button_tones[i], 0.25) - break - else: - magtag.peripherals.neopixel_disable = True - - if (time.monotonic() - timestamp) > 5: # once a second... - print("Battery voltage? ", magtag.peripherals.battery) - while not i2c.try_lock(): - pass - print( - "I2C addresses found:", - [hex(device_address) for device_address in i2c.scan()], - ) - i2c.unlock() - try: - value = magtag.fetch() - print("Response is", value) - except (ValueError, RuntimeError) as e: - print("Some error occured, retrying! -", e) - timestamp = time.monotonic() - - time.sleep(0.01) diff --git a/examples/magtag_simpletest.py b/examples/magtag_simpletest.py index 0d4a377..4a8a646 100644 --- a/examples/magtag_simpletest.py +++ b/examples/magtag_simpletest.py @@ -1,3 +1,36 @@ # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # # SPDX-License-Identifier: Unlicense +import time +import terminalio +from adafruit_magtag.magtag import MagTag + +magtag = MagTag() + +magtag.add_text( + text_font=terminalio.FONT, + text_position=( + 50, + (magtag.graphics.display.height // 2) - 1, + ), + text_scale=3, +) + +magtag.set_text("Hello World") + +buttons = magtag.peripherals.buttons +button_colors = ((255, 0, 0), (255, 150, 0), (0, 255, 255), (180, 0, 255)) +button_tones = (1047, 1318, 1568, 2093) +timestamp = time.monotonic() + +while True: + for i, b in enumerate(buttons): + if not b.value: + print("Button %c pressed" % chr((ord("A") + i))) + magtag.peripherals.neopixel_disable = False + magtag.peripherals.neopixels.fill(button_colors[i]) + magtag.peripherals.play_tone(button_tones[i], 0.25) + break + else: + magtag.peripherals.neopixel_disable = True + time.sleep(0.01) diff --git a/setup.py b/setup.py deleted file mode 100644 index d601458..0000000 --- a/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -"""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-circuitpython-magtag", - use_scm_version=True, - setup_requires=["setuptools_scm"], - description="Helper library for the Adafruit MagTag.", - long_description=long_description, - long_description_content_type="text/x-rst", - # The project's main homepage. - url="https://github.com/adafruit/Adafruit_CircuitPython_MagTag", - # Author details - author="Adafruit Industries", - author_email="circuitpython@adafruit.com", - install_requires=[ - "Adafruit-Blinka", - "adafruit-blinka-displayio", - "adafruit-circuitpython-bitmap-font", - "adafruit-circuitpython-display-text", - "adafruit-circuitpython-neopixel", - "adafruit-circuitpython-requests", - ], - # 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 magtag eInk ePaper EPD Portal", - # You can just specify the packages manually here if your project is - # simple. Or you can use find_packages(). - packages=["adafruit_magtag"], -) diff --git a/setup.py.disabled b/setup.py.disabled new file mode 100644 index 0000000..4d6d42a --- /dev/null +++ b/setup.py.disabled @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +This library is not deployed to PyPI. It is either a board-specific helper library, or +does not make sense for use on or is incompatible with single board computers and Linux. +""" From 77dcaf54d9dec90ef94c4ebeae55158304174c53 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 13 Nov 2020 10:46:34 -0800 Subject: [PATCH 6/9] Black formatted --- examples/bitcoin_demo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/bitcoin_demo.py b/examples/bitcoin_demo.py index 116b3d8..6fdd13a 100644 --- a/examples/bitcoin_demo.py +++ b/examples/bitcoin_demo.py @@ -9,11 +9,14 @@ DATA_SOURCE = "https://api.coindesk.com/v1/bpi/currentprice.json" DATA_LOCATION = ["bpi", "USD", "rate_float"] + def text_transform(val): return "Bitcoin: $%d" % val + magtag = MagTag( - url=DATA_SOURCE, json_path=DATA_LOCATION, + url=DATA_SOURCE, + json_path=DATA_LOCATION, ) magtag.network.connect() From d2da47e889a26d6427cd0a15281781c9c74d2061 Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 13 Nov 2020 10:54:24 -0800 Subject: [PATCH 7/9] Add some missing requirements --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index f01609c..2e16076 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ adafruit-circuitpython-bitmap-font adafruit-circuitpython-display-text adafruit-circuitpython-neopixel adafruit-circuitpython-requests +adafruit-circuitpython-adafruitio +adafruit-circuitpython-il0373 From fbe2a3c40e7d9365d07f5e8a0480af6371d2f48a Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 13 Nov 2020 11:03:22 -0800 Subject: [PATCH 8/9] Add some more missing requirements --- docs/conf.py | 10 +++++++++- requirements.txt | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 5cea527..ea72ffe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,15 @@ # 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 = ["supervisor", "rtc", "ssl", "wifi", "socketpool", "secrets"] +autodoc_mock_imports = [ + "supervisor", + "rtc", + "ssl", + "wifi", + "socketpool", + "secrets", + "analogio", +] intersphinx_mapping = { diff --git a/requirements.txt b/requirements.txt index 2e16076..7d663bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ adafruit-circuitpython-neopixel adafruit-circuitpython-requests adafruit-circuitpython-adafruitio adafruit-circuitpython-il0373 +adafruit-circuitpython-simpleio From fe3a06fbce34b798ad033d0d9e63a61bde275a0b Mon Sep 17 00:00:00 2001 From: Melissa LeBlanc-Williams Date: Fri, 13 Nov 2020 13:05:51 -0800 Subject: [PATCH 9/9] Updated bitcoin example to update every 60 seconds + first try --- examples/bitcoin_demo.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/bitcoin_demo.py b/examples/bitcoin_demo.py index 6fdd13a..6047029 100644 --- a/examples/bitcoin_demo.py +++ b/examples/bitcoin_demo.py @@ -34,15 +34,13 @@ def text_transform(val): magtag.preload_font(b"$012345789") # preload numbers magtag.preload_font((0x00A3, 0x20AC)) # preload gbp/euro symbol -timestamp = time.monotonic() +timestamp = None while True: - if (time.monotonic() - timestamp) > 5: # once a second... + if not timestamp or (time.monotonic() - timestamp) > 60: # once every 60 seconds... try: value = magtag.fetch() print("Response is", value) except (ValueError, RuntimeError) as e: print("Some error occured, retrying! -", e) timestamp = time.monotonic() - - time.sleep(0.01)