├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── LICENSE ├── README.rst ├── circuitpython_cirque_pinnacle.py ├── cspell.config.yml ├── docs ├── _static │ ├── Cirque_GlidePoint-Circle-Trackpad.png │ ├── Logo.png │ ├── extra_css.css │ └── favicon.ico ├── anymeas.rst ├── api.rst ├── conf.py ├── contributing.rst ├── examples.rst ├── index.rst ├── rel_abs.rst └── requirements.txt ├── examples ├── cirque_pinnacle_absolute_mode.py ├── cirque_pinnacle_anymeas_mode.py ├── cirque_pinnacle_relative_mode.py └── cirque_pinnacle_usb_mouse.py ├── pyproject.toml ├── requirements.txt └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: 2bndy5 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://www.paypal.me/Brendan884"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | # Workflow files stored in the 10 | # default location of `.github/workflows` 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | push: 7 | branches: [master] 8 | 9 | jobs: 10 | build-wheel: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.x" 16 | 17 | - uses: actions/checkout@v4 18 | 19 | - name: Build wheel 20 | run: pip wheel -w dist --no-deps . 21 | 22 | - name: check dist 23 | run: pipx run twine check dist/* 24 | 25 | - name: Archive wheel 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: wheel 29 | path: ${{ github.workspace }}/dist/ 30 | 31 | linters: 32 | runs-on: ubuntu-latest 33 | steps: 34 | 35 | - uses: actions/setup-python@v5 36 | with: 37 | python-version: "3.x" 38 | 39 | - uses: actions/checkout@v4 40 | 41 | - name: Install pre-commit and deps 42 | run: pip install pre-commit -r requirements.txt 43 | 44 | - name: Setup problem matchers 45 | uses: adafruit/circuitpython-action-library-ci-problem-matchers@v1 46 | 47 | - name: Pre-commit hooks 48 | run: pre-commit run --all-files 49 | 50 | build-bundles: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Translate Repo Name For Build Tools filename_prefix 54 | id: repo-name 55 | run: | 56 | echo repo-name=$( 57 | echo ${{ github.repository }} | 58 | awk -F '\/' '{ print tolower($2) }' | 59 | tr '_' '-' 60 | ) >> $GITHUB_OUTPUT 61 | 62 | - uses: actions/checkout@v4 63 | 64 | - name: Set up Python 3.x 65 | uses: actions/setup-python@v5 66 | with: 67 | python-version: "3.11" 68 | 69 | - name: Checkout tools repo 70 | uses: actions/checkout@v4 71 | with: 72 | repository: adafruit/actions-ci-circuitpython-libs 73 | path: actions-ci 74 | 75 | - name: Install deps 76 | run: | 77 | source actions-ci/install.sh 78 | 79 | - name: Build assets 80 | run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . 81 | 82 | - name: Archive bundles 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: bundles 86 | path: ${{ github.workspace }}/bundles/ 87 | 88 | build-docs: 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: actions/checkout@v4 92 | 93 | - name: Set up Python 3.x 94 | uses: actions/setup-python@v5 95 | with: 96 | python-version: "3.x" 97 | 98 | - name: Install deps 99 | run: | 100 | pip install -r docs/requirements.txt -r requirements.txt 101 | 102 | - name: Build docs 103 | working-directory: docs 104 | run: sphinx-build -E -W -b html . _build/html 105 | 106 | - name: Archive docs 107 | uses: actions/upload-artifact@v4 108 | with: 109 | name: docs 110 | path: ${{ github.workspace }}/docs/_build/html 111 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Actions 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | upload-release-assets: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Translate Repo Name For Build Tools filename_prefix 13 | id: repo-name 14 | run: | 15 | echo repo-name=$( 16 | echo ${{ github.repository }} | 17 | awk -F '\/' '{ print tolower($2) }' | 18 | tr '_' '-' 19 | ) >> $GITHUB_OUTPUT 20 | 21 | - name: Set up Python 3.x 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.x" 25 | 26 | - name: Checkout Current Repo 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Checkout tools repo 32 | uses: actions/checkout@v4 33 | with: 34 | repository: adafruit/actions-ci-circuitpython-libs 35 | path: actions-ci 36 | 37 | - name: Install deps 38 | run: | 39 | source actions-ci/install.sh 40 | 41 | - name: Build assets 42 | run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . 43 | 44 | - name: Archive bundles 45 | if: github.event_name == 'workflow_dispatch' 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: bundles 49 | path: ${{ github.workspace }}/bundles/ 50 | 51 | - name: Upload Release Assets 52 | if: github.event_name == 'release' 53 | uses: shogo82148/actions-upload-release-asset@v1 54 | with: 55 | upload_url: ${{ github.event.release.upload_url }} 56 | asset_path: "bundles/*" 57 | 58 | upload-pypi: 59 | runs-on: ubuntu-latest 60 | permissions: 61 | id-token: write 62 | steps: 63 | - uses: actions/checkout@v4 64 | with: 65 | fetch-depth: 0 66 | 67 | - uses: actions/setup-python@v5 68 | with: 69 | python-version: '3.x' 70 | 71 | - name: Install build tools 72 | run: | 73 | python -m pip install --upgrade pip 74 | pip install build twine 75 | 76 | - name: Build distributions 77 | run: python -m build 78 | 79 | - name: Check distributions 80 | run: twine check dist/* 81 | 82 | - name: Publish package (to TestPyPI) 83 | if: github.event_name == 'workflow_dispatch' && github.repository == '2bndy5/CircuitPython_Cirque_Pinnacle' 84 | uses: pypa/gh-action-pypi-publish@v1.12.4 85 | with: 86 | repository-url: https://test.pypi.org/legacy/ 87 | 88 | - name: Publish package (to PyPI) 89 | if: github.event_name != 'workflow_dispatch' && github.repository == '2bndy5/CircuitPython_Cirque_Pinnacle' 90 | uses: pypa/gh-action-pypi-publish@v1.12.4 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build*/ 12 | bundles/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | 128 | # VS Code folder 129 | .vscode 130 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/astral-sh/ruff-pre-commit 9 | rev: v0.11.4 10 | hooks: 11 | # Run the linter. 12 | - id: ruff 13 | # Run the formatter. 14 | - id: ruff-format 15 | - repo: https://github.com/pre-commit/mirrors-mypy 16 | rev: v1.15.0 17 | hooks: 18 | - id: mypy 19 | name: mypy (library code) 20 | exclude: "^(docs/|examples/|tests/|setup.py$)" 21 | types: [python] 22 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | # Path to your Sphinx configuration file. 5 | configuration: docs/conf.py 6 | 7 | build: 8 | os: "ubuntu-24.04" 9 | tools: 10 | python: "latest" 11 | 12 | python: 13 | install: 14 | - requirements: docs/requirements.txt 15 | - method: pip 16 | path: . 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adafruit Community Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and leaders pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level or type of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | We are committed to providing a friendly, safe and welcoming environment for 15 | all. 16 | 17 | Examples of behavior that contributes to creating a positive environment 18 | include: 19 | 20 | * Be kind and courteous to others 21 | * Using welcoming and inclusive language 22 | * Being respectful of differing viewpoints and experiences 23 | * Collaborating with other community members 24 | * Gracefully accepting constructive criticism 25 | * Focusing on what is best for the community 26 | * Showing empathy towards other community members 27 | 28 | Examples of unacceptable behavior by participants include: 29 | 30 | * The use of sexualized language or imagery and sexual attention or advances 31 | * The use of inappropriate images, including in a community member's avatar 32 | * The use of inappropriate language, including in a community member's nickname 33 | * Any spamming, flaming, baiting or other attention-stealing behavior 34 | * Excessive or unwelcome helping; answering outside the scope of the question 35 | asked 36 | * Trolling, insulting/derogatory comments, and personal or political attacks 37 | * Public or private harassment 38 | * Publishing others' private information, such as a physical or electronic 39 | address, without explicit permission 40 | * Other conduct which could reasonably be considered inappropriate 41 | 42 | The goal of the standards and moderation guidelines outlined here is to build 43 | and maintain a respectful community. We ask that you don’t just aim to be 44 | "technically unimpeachable", but rather try to be your best self. 45 | 46 | We value many things beyond technical expertise, including collaboration and 47 | supporting others within our community. Providing a positive experience for 48 | other community members can have a much more significant impact than simply 49 | providing the correct answer. 50 | 51 | ## Our Responsibilities 52 | 53 | Project leaders are responsible for clarifying the standards of acceptable 54 | behavior and are expected to take appropriate and fair corrective action in 55 | response to any instances of unacceptable behavior. 56 | 57 | Project leaders have the right and responsibility to remove, edit, or 58 | reject messages, comments, commits, code, issues, and other contributions 59 | that are not aligned to this Code of Conduct, or to ban temporarily or 60 | permanently any community member for other behaviors that they deem 61 | inappropriate, threatening, offensive, or harmful. 62 | 63 | ## Moderation 64 | 65 | Instances of behaviors that violate the Adafruit Community Code of Conduct 66 | may be reported by any member of the community. Community members are 67 | encouraged to report these situations, including situations they witness 68 | involving other community members. 69 | 70 | You may report in the following ways: 71 | 72 | In any situation, you may send an email to . 73 | 74 | On the Adafruit Discord, you may send an open message from any channel 75 | to all Community Helpers by tagging @community moderators. You may also send an 76 | open message from any channel, or a direct message to @kattni#1507, 77 | @tannewt#4653, @Dan Halbert#1614, @cater#2442, @sommersoft#0222, or 78 | @Andon#8175. 79 | 80 | Email and direct message reports will be kept confidential. 81 | 82 | In situations on Discord where the issue is particularly egregious, possibly 83 | illegal, requires immediate action, or violates the Discord terms of service, 84 | you should also report the message directly to Discord. 85 | 86 | These are the steps for upholding our community’s standards of conduct. 87 | 88 | 1. Any member of the community may report any situation that violates the 89 | Adafruit Community Code of Conduct. All reports will be reviewed and 90 | investigated. 91 | 2. If the behavior is an egregious violation, the community member who 92 | committed the violation may be banned immediately, without warning. 93 | 3. Otherwise, moderators will first respond to such behavior with a warning. 94 | 4. Moderators follow a soft "three strikes" policy - the community member may 95 | be given another chance, if they are receptive to the warning and change their 96 | behavior. 97 | 5. If the community member is unreceptive or unreasonable when warned by a 98 | moderator, or the warning goes unheeded, they may be banned for a first or 99 | second offense. Repeated offenses will result in the community member being 100 | banned. 101 | 102 | ## Scope 103 | 104 | This Code of Conduct and the enforcement policies listed above apply to all 105 | Adafruit Community venues. This includes but is not limited to any community 106 | spaces (both public and private), the entire Adafruit Discord server, and 107 | Adafruit GitHub repositories. Examples of Adafruit Community spaces include 108 | but are not limited to meet-ups, audio chats on the Adafruit Discord, or 109 | interaction at a conference. 110 | 111 | This Code of Conduct applies both within project spaces and in public spaces 112 | when an individual is representing the project or its community. As a community 113 | member, you are representing our community, and are expected to behave 114 | accordingly. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 1.4, available at 120 | , 121 | and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). 122 | 123 | For other projects adopting the Adafruit Community Code of 124 | Conduct, please contact the maintainers of those projects for enforcement. 125 | If you wish to use this code of conduct for your own project, consider 126 | explicitly mentioning your moderation policy or making a copy with your 127 | own moderation policy so as to avoid confusion. 128 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | 2 | Contributing Guidelines 3 | ======================= 4 | 5 | Linting the source code 6 | ----------------------- 7 | 8 | .. _pre-commit: https://pre-commit.com/ 9 | 10 | This library uses pre-commit_ for some linting tools like 11 | 12 | - `black `_ 13 | - `pylint `_ 14 | - `mypy `_ 15 | 16 | To use pre-commit_, you must install it and create the cached environments that it needs. 17 | 18 | .. code-block:: shell 19 | 20 | pip install pre-commit 21 | pre-commit install 22 | 23 | Now, every time you commit something it will run pre-commit_ on the changed files. You can also 24 | run pre-commit_ on staged files: 25 | 26 | .. code-block:: shell 27 | 28 | pre-commit run 29 | 30 | .. note:: 31 | Use the ``--all-files`` argument to run pre-commit on all files in the repository. 32 | 33 | 34 | Building the Documentation 35 | -------------------------- 36 | 37 | To build library documentation, you need to install the documentation dependencies. 38 | 39 | .. code-block:: shell 40 | 41 | pip install -r docs/requirements.txt 42 | 43 | Finally, build the documentation with Sphinx: 44 | 45 | .. code-block:: shell 46 | 47 | sphinx-build -E -W docs docs/_build/html 48 | 49 | The rendered HTML files should now be located in the ``docs/_build/html`` folder. Point your 50 | internet browser to this path and check the changes have been rendered properly. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brendan Doherty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: https://readthedocs.org/projects/circuitpython-cirque-pinnacle/badge/?version=latest 3 | :target: https://circuitpython-cirque-pinnacle.readthedocs.io/en/latest/?badge=latest 4 | :alt: Documentation Status 5 | 6 | .. image:: https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle/workflows/Build%20CI/badge.svg 7 | :target: https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle/actions/ 8 | :alt: Build Status 9 | 10 | .. image:: https://img.shields.io/pypi/v/circuitpython-cirque-pinnacle.svg 11 | :alt: latest version on PyPI 12 | :target: https://pypi.python.org/pypi/circuitpython-cirque-pinnacle 13 | 14 | .. image:: https://static.pepy.tech/personalized-badge/circuitpython-cirque-pinnacle?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Pypi%20Downloads 15 | :alt: Total PyPI downloads 16 | :target: https://pepy.tech/project/circuitpython-cirque-pinnacle 17 | 18 | A circuitpython library for using the Cirque Glidepoint circle trackpads (as seen in the Steam Controller and Valve\HTC Vive VR controllers). 19 | 20 | Read the `Documentation at ReadTheDocs.org `_ 21 | -------------------------------------------------------------------------------- /circuitpython_cirque_pinnacle.py: -------------------------------------------------------------------------------- 1 | """ 2 | A driver module for the Cirque Pinnacle ASIC on the Cirque capacitive touch 3 | based circular trackpads. 4 | """ 5 | 6 | __version__ = "0.0.0-auto.0" 7 | __repo__ = "https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle.git" 8 | import time 9 | import struct 10 | 11 | try: 12 | from typing import Optional, List, Union, Iterable 13 | except ImportError: 14 | pass 15 | 16 | from micropython import const 17 | import digitalio 18 | import busio 19 | from adafruit_bus_device.spi_device import SPIDevice 20 | from adafruit_bus_device.i2c_device import I2CDevice 21 | 22 | #: A mode to measure changes in X & Y axis positions. See :doc:`rel_abs`. 23 | PINNACLE_RELATIVE: int = const(0x00) 24 | #: A mode for raw ADC measurements. See :doc:`anymeas`. 25 | PINNACLE_ANYMEAS: int = const(0x01) 26 | #: A mode to measure X, Y, & Z axis positions. See :doc:`rel_abs`. 27 | PINNACLE_ABSOLUTE: int = const(0x02) 28 | PINNACLE_GAIN_100: int = const(0xC0) #: around 100% gain 29 | PINNACLE_GAIN_133: int = const(0x80) #: around 133% gain 30 | PINNACLE_GAIN_166: int = const(0x40) #: around 166% gain 31 | PINNACLE_GAIN_200: int = const(0x00) #: around 200% gain 32 | PINNACLE_FREQ_0: int = const(0x02) #: frequency around 500,000 Hz 33 | PINNACLE_FREQ_1: int = const(0x03) #: frequency around 444,444 Hz 34 | PINNACLE_FREQ_2: int = const(0x04) #: frequency around 400,000 Hz 35 | PINNACLE_FREQ_3: int = const(0x05) #: frequency around 363,636 Hz 36 | PINNACLE_FREQ_4: int = const(0x06) #: frequency around 333,333 Hz 37 | PINNACLE_FREQ_5: int = const(0x07) #: frequency around 307,692 Hz 38 | PINNACLE_FREQ_6: int = const(0x09) #: frequency around 267,000 Hz 39 | PINNACLE_FREQ_7: int = const(0x0B) #: frequency around 235,000 Hz 40 | PINNACLE_MUX_REF1: int = const(0x10) #: enables a builtin capacitor (~0.5 pF). 41 | PINNACLE_MUX_REF0: int = const(0x08) #: enables a builtin capacitor (~0.25 pF). 42 | PINNACLE_MUX_PNP: int = const(0x04) #: enable PNP sense line 43 | PINNACLE_MUX_NPN: int = const(0x01) #: enable NPN sense line 44 | PINNACLE_CTRL_REPEAT: int = const(0x80) #: required for more than 1 measurement 45 | #: triggers low power mode (sleep) after completing measurements 46 | PINNACLE_CTRL_PWR_IDLE: int = const(0x40) 47 | 48 | # Defined constants for Pinnacle registers 49 | _FIRMWARE_ID: int = const(0x00) 50 | _STATUS: int = const(0x02) 51 | _SYS_CONFIG: int = const(0x03) 52 | _FEED_CONFIG_1: int = const(0x04) 53 | _FEED_CONFIG_2: int = const(0x05) 54 | _FEED_CONFIG_3: int = const(0x06) 55 | _CAL_CONFIG: int = const(0x07) 56 | _SAMPLE_RATE: int = const(0x09) 57 | _Z_IDLE: int = const(0x0A) 58 | # _Z_SCALER: int = const(0x0B) 59 | # _SLEEP_INTERVAL: int = const(0x0C) # time of sleep until checking for finger 60 | # _SLEEP_TIMER: int = const(0x0D) # time after idle mode until sleep starts 61 | _PACKET_BYTE_0: int = const(0x12) 62 | _PACKET_BYTE_1: int = const(0x13) 63 | _ERA_VALUE: int = const(0x1B) 64 | _ERA_ADDR: int = const(0x1C) 65 | _ERA_CONTROL: int = const(0x1E) 66 | _HCO_ID: int = const(0x1F) 67 | 68 | 69 | class AbsoluteReport: 70 | """A class to represent data reported by `PinnacleTouch.read()` when 71 | `PinnacleTouch.data_mode` is set to `PINNACLE_ABSOLUTE`. 72 | 73 | Each parameter is used as the initial value for the corresponding attribute. 74 | If not specified, then the attribute is set to ``0``. 75 | """ 76 | 77 | def __init__(self, buttons: int = 0, x: int = 0, y: int = 0, z: int = 0): 78 | self.buttons: int = buttons 79 | """The button data is a byte in which each bit represents a button. 80 | The bit to button order is as follows: 81 | 82 | 0. [LSBit] Button 1. 83 | 1. Button 2. 84 | 2. Button 3. 85 | """ 86 | self.x: int = x 87 | """The position on the X axis ranging [0, 2047]. The datasheet recommends the 88 | X-axis value should be clamped to the range [128, 1920] for reliability.""" 89 | self.y: int = y 90 | """The position on the Y axis ranging [0, 1535]. The datasheet recommends the 91 | Y-axis value should be clamped to the range [64, 1472] for reliability.""" 92 | self.z: int = z 93 | """The magnitude of the Z axis (ranging [0, 255]) can be used as the proximity 94 | of the finger to the trackpad. ``0`` means no proximity. The maximum value 95 | reported may be influenced by `set_adc_gain()`.""" 96 | 97 | def __repr__(self) -> str: 98 | return "".format( 99 | self.buttons & 1, 100 | self.buttons & 2, 101 | self.buttons & 4, 102 | self.x, 103 | self.y, 104 | self.z, 105 | ) 106 | 107 | 108 | class RelativeReport: 109 | """A class to represent data reported by `PinnacleTouch.read()` when 110 | `PinnacleTouch.data_mode` is set to `PINNACLE_RELATIVE`. 111 | 112 | :param buf: A buffer object used to unpack initial values for the `buttons`, `x`, 113 | `y`, and `scroll` attributes. If not specified, then all attributes are set to 114 | ``0``. 115 | """ 116 | 117 | def __init__(self, buf: Union[bytes, bytearray] = b"\0\0\0\0"): 118 | data = struct.unpack("Bbbb", buf[:4]) 119 | self.buttons: int = data[0] 120 | """The button data is a byte in which each bit represents a button. 121 | The bit to button order is as follows: 122 | 123 | 0. [LSBit] Button 1 (thought of as Left button on a mouse). If the ``taps`` 124 | parameter is ``True`` when calling `relative_mode_config()`, a single tap 125 | will be reflected here. 126 | 1. Button 2 (thought of as Right button on a mouse). If ``taps`` and 127 | ``secondary_tap`` parameters are ``True`` when calling 128 | `relative_mode_config()`, a single tap in the perspective top-left-most 129 | corner will be reflected here; secondary taps are constantly disabled if 130 | `hard_configured` returns ``True``. Note that the top-left-most corner can be 131 | perspectively moved if ``rotate90`` parameter is ``True`` when calling 132 | `relative_mode_config()`. 133 | 2. Button 3 (thought of as Middle or scroll wheel button on a mouse) 134 | """ 135 | self.x: int = data[1] #: The change in X-axis ranging [-127, 127]. 136 | self.y: int = data[2] #: The change in Y-axis ranging [-127, 127]. 137 | self.scroll: int = data[3] 138 | """The change in scroll counter ranging [-127, 127]. This data is only reported 139 | if the ``intellimouse`` parameter is ``True`` to `relative_mode_config()`. 140 | """ 141 | 142 | @property 143 | def buffer(self) -> bytes: 144 | """A read-only property to return a `bytes` object that can be used as a Mouse 145 | HID report buffer.""" 146 | return struct.pack("Bbbb", self.buttons, self.x, self.y, self.scroll) 147 | 148 | def __repr__(self) -> str: 149 | return ( 150 | "".format( 152 | self.buttons & 1, 153 | self.buttons & 2, 154 | self.buttons & 4, 155 | self.x, 156 | self.y, 157 | self.scroll, 158 | ) 159 | ) 160 | 161 | 162 | class PinnacleTouch: 163 | """The abstract base class for driving the Pinnacle ASIC. 164 | 165 | :param dr_pin: |dr_pin_parameter| 166 | 167 | .. versionchanged:: 2.0.0 ``dr_pin`` is a required parameter. 168 | 169 | |dr_pin_required| 170 | 171 | .. |dr_pin_parameter| replace:: The input pin connected to the Pinnacle ASIC's "Data 172 | Ready" pin. 173 | .. |dr_pin_required| replace:: 174 | Previously, this parameter was conditionally optional. 175 | """ 176 | 177 | def __init__(self, dr_pin: digitalio.DigitalInOut): 178 | self.dr_pin = dr_pin 179 | self.dr_pin.switch_to_input() 180 | firmware_id, firmware_ver = self._rap_read_bytes(_FIRMWARE_ID, 2) 181 | self._rev2025: bool = firmware_id == 0x0E and firmware_ver == 0x75 182 | if not self._rev2025 and (firmware_id, firmware_ver) != (7, 0x3A): 183 | raise RuntimeError("Cirque Pinnacle ASIC not responding") 184 | self._intellimouse = False 185 | self._mode = PINNACLE_RELATIVE 186 | if not self._rev2025: 187 | self.detect_finger_stylus() 188 | else: 189 | self.sample_rate = 100 190 | self._rap_write(_Z_IDLE, 30) # z-idle packet count 191 | self._rap_write_bytes(_SYS_CONFIG, bytes(3)) # config data mode, power, etc 192 | if not self._rev2025: 193 | self.set_adc_gain(0) 194 | while self.available(): 195 | self.clear_status_flags() 196 | if not self.calibrate(): 197 | raise AttributeError( 198 | "Calibration did not complete. Check wiring to `dr_pin`." 199 | ) 200 | self.feed_enable = True 201 | 202 | @property 203 | def rev2025(self) -> bool: 204 | """Is this trackpad using a newer firmware version? 205 | 206 | This read-only property describes if the Pinnacle ASIC uses a firmware revision 207 | that was deployed on or around 2025. Consequently, some advanced configuration 208 | is not possible with this undocumented firmware revision. 209 | Thus, the following functionality is affected on the trackpads when this 210 | property returns ``True``: 211 | 212 | - :py:attr:`~circuitpython_cirque_pinnacle.PinnacleTouch.sample_rate` cannot 213 | exceed ``100`` 214 | - :py:attr:`~circuitpython_cirque_pinnacle.PinnacleTouch.detect_finger_stylus()` 215 | is non-operational 216 | - :py:meth:`~circuitpython_cirque_pinnacle.PinnacleTouch.tune_edge_sensitivity()` 217 | is non-operational 218 | - :py:meth:`~circuitpython_cirque_pinnacle.PinnacleTouch.set_adc_gain()` 219 | is non-operational 220 | - :py:attr:`~circuitpython_cirque_pinnacle.PinnacleTouch.calibration_matrix` 221 | is non-operational 222 | 223 | .. versionadded:: 2.0.0 224 | """ 225 | return self._rev2025 226 | 227 | @property 228 | def feed_enable(self) -> bool: 229 | """This `bool` attribute controls if the touch/button event data is 230 | reported (``True``) or not (``False``). 231 | 232 | This function only applies to `PINNACLE_RELATIVE` or `PINNACLE_ABSOLUTE` mode. 233 | Otherwise if `data_mode` is set to `PINNACLE_ANYMEAS`, then this attribute will 234 | have no effect. 235 | """ 236 | return bool(self._rap_read(_FEED_CONFIG_1) & 1) 237 | 238 | @feed_enable.setter 239 | def feed_enable(self, is_on: bool): 240 | is_enabled = self._rap_read(_FEED_CONFIG_1) 241 | if bool(is_enabled & 1) != is_on: 242 | # save ourselves the unnecessary transaction 243 | is_enabled = (is_enabled & 0xFE) | bool(is_on) 244 | self._rap_write(_FEED_CONFIG_1, is_enabled) 245 | 246 | @property 247 | def data_mode(self) -> int: 248 | """This attribute controls the mode for which kind of data to report. The 249 | supported modes are `PINNACLE_RELATIVE`, `PINNACLE_ANYMEAS`, 250 | `PINNACLE_ABSOLUTE`. Default is `PINNACLE_RELATIVE`. 251 | 252 | .. important:: 253 | When switching from `PINNACLE_ANYMEAS` to `PINNACLE_RELATIVE` or 254 | `PINNACLE_ABSOLUTE`, all configurations are reset, and must be re-configured 255 | by using `absolute_mode_config()` or `relative_mode_config()`. 256 | """ 257 | return self._mode 258 | 259 | @data_mode.setter 260 | def data_mode(self, mode: int): 261 | if mode not in (PINNACLE_ANYMEAS, PINNACLE_RELATIVE, PINNACLE_ABSOLUTE): 262 | raise ValueError("Unrecognized input value for data_mode.") 263 | sys_config = self._rap_read(_SYS_CONFIG) & 0xE7 # clear AnyMeas mode flags 264 | if mode in (PINNACLE_RELATIVE, PINNACLE_ABSOLUTE): 265 | if self._mode == PINNACLE_ANYMEAS: # if leaving AnyMeas mode 266 | self._rap_write(_CAL_CONFIG, 0x1E) # enables all compensations 267 | self._rap_write(_Z_IDLE, 30) # 30 z-idle packets 268 | self._mode = mode 269 | self.sample_rate = 100 270 | # set mode flag, enable feed, disable taps in Relative mode 271 | self._rap_write_bytes(_SYS_CONFIG, bytes([sys_config, 1 | mode, 2])) 272 | else: # not leaving AnyMeas mode 273 | self._mode = mode 274 | self._rap_write(_FEED_CONFIG_1, 1 | mode) # set mode flag, enable feed 275 | self._intellimouse = False 276 | else: # for AnyMeas mode 277 | # disable tracking computations for AnyMeas mode 278 | self._rap_write(_SYS_CONFIG, sys_config | 0x08) 279 | time.sleep(0.01) # wait for tracking computations to expire 280 | self._mode = mode 281 | self.anymeas_mode_config() # configure registers for AnyMeas 282 | 283 | @property 284 | def hard_configured(self) -> bool: 285 | """This read-only `bool` attribute can be used to inform applications about 286 | factory customized hardware configuration. See note about product labeling in 287 | `Model Labeling Scheme `. 288 | 289 | :Returns: 290 | ``True`` if a 470K ohm resistor is populated at the junction labeled "R4" 291 | """ 292 | return bool(self._rap_read(_HCO_ID) & 0x80) 293 | 294 | def relative_mode_config( 295 | self, 296 | taps: bool = True, 297 | rotate90: bool = False, 298 | secondary_tap: bool = True, 299 | intellimouse: bool = False, 300 | glide_extend: bool = False, 301 | ): 302 | """Configure settings specific to Relative mode (AKA Mouse mode) data 303 | reporting. 304 | 305 | This function only applies to `PINNACLE_RELATIVE` mode, otherwise if `data_mode` 306 | is set to `PINNACLE_ANYMEAS` or `PINNACLE_ABSOLUTE`, then this function does 307 | nothing. 308 | 309 | :param taps: Specifies if all taps should be reported (``True``) or not 310 | (``False``). Default is ``True``. This affects the ``secondary_tap`` 311 | parameter as well. 312 | :param rotate90: Specifies if the axis data is altered for 90 degree rotation 313 | before reporting it (essentially swaps the axis data). Default is ``False``. 314 | :param secondary_tap: Specifies if tapping in the top-left corner (depending on 315 | orientation) triggers the secondary button data. Defaults to ``True``. This 316 | feature is always disabled if `hard_configured` is ``True``. 317 | :param intellimouse: Specifies if the data reported includes a byte about scroll 318 | data. Default is ``False``. Because this flag is specific to scroll data, 319 | this feature is always disabled if `hard_configured` is ``True``. 320 | :param glide_extend: A patented feature that allows the user to glide their 321 | finger off the edge of the sensor and continue gesture with the touch event. 322 | Default is ``False``. This feature is always disabled if `hard_configured` 323 | is ``True``. 324 | """ 325 | if self._mode == PINNACLE_RELATIVE: 326 | config2 = (rotate90 << 7) | ((not glide_extend) << 4) 327 | config2 |= ((not secondary_tap) << 2) | ((not taps) << 1) 328 | self._rap_write(_FEED_CONFIG_2, config2 | bool(intellimouse)) 329 | if intellimouse: 330 | # send required cmd to enable intellimouse 331 | req_seq = bytes([0xF3, 0xC8, 0xF3, 0x64, 0xF3, 0x50]) 332 | self._rap_write_cmd(req_seq) 333 | # verify w/ cmd to read the device ID 334 | response = self._rap_read_bytes(0xF2, 3) 335 | self._intellimouse = response.startswith(b"\xf3\x03") 336 | 337 | def absolute_mode_config( 338 | self, z_idle_count: int = 30, invert_x: bool = False, invert_y: bool = False 339 | ): 340 | """Configure settings specific to Absolute mode (reports axis 341 | positions). 342 | 343 | This function only applies to `PINNACLE_ABSOLUTE` mode, otherwise if `data_mode` 344 | is set to `PINNACLE_ANYMEAS` or `PINNACLE_RELATIVE`, then this function does 345 | nothing. 346 | 347 | :param z_idle_count: Specifies the number of empty packets (x-axis, y-axis, and 348 | z-axis are ``0``) reported (every 10 milliseconds) when there is no touch 349 | detected. Defaults to 30. This number is clamped to range [0, 255]. 350 | :param invert_x: Specifies if the x-axis data is to be inverted before reporting 351 | it. Default is ``False``. 352 | :param invert_y: Specifies if the y-axis data is to be inverted before reporting 353 | it. Default is ``False``. 354 | """ 355 | if self._mode == PINNACLE_ABSOLUTE: 356 | self._rap_write(_Z_IDLE, max(0, min(z_idle_count, 255))) 357 | config1 = self._rap_read(_FEED_CONFIG_1) & 0x3F | (invert_y << 7) 358 | self._rap_write(_FEED_CONFIG_1, config1 | (invert_x << 6)) 359 | 360 | def available(self) -> bool: 361 | """Determine if there is fresh data to report. 362 | 363 | This is just a convenience method to check the ``dr_pin.value``. 364 | 365 | :Returns: ``True`` if there is fresh data to report, otherwise ``False``. 366 | """ 367 | return self.dr_pin.value 368 | 369 | def read( 370 | self, report: Union[AbsoluteReport, RelativeReport], read_buttons: bool = True 371 | ) -> None: 372 | """This function will return touch (& button) event data from the Pinnacle ASIC. 373 | 374 | This function only applies to `PINNACLE_RELATIVE` or `PINNACLE_ABSOLUTE` mode. 375 | Otherwise if `data_mode` is set to `PINNACLE_ANYMEAS`, then this function 376 | does nothing. 377 | 378 | :param report: A `AbsoluteReport` or `RelativeReport` object (depending on the 379 | currently set `data_mode`) that is used to store the described touch and/or 380 | button event data. 381 | :param read_buttons: A flag that can be used to skip reading the button data 382 | from the Pinnacle. Default (``True``) will read the button data and store it 383 | in the ``report`` object's :attr:`~RelativeReport.buttons` attribute. This 384 | is really only useful to speed up read operations when not using the 385 | Pinnacle's button input pins. 386 | 387 | .. warning:: 388 | If `PINNACLE_RELATIVE` mode's tap detection is enabled, then setting 389 | this parameter to ``False`` can be deceptively inaccurate when reporting 390 | tap gestures. 391 | """ 392 | if self._mode == PINNACLE_ABSOLUTE: # if absolute mode 393 | skip = (not read_buttons) * 2 394 | data = self._rap_read_bytes(_PACKET_BYTE_0 + skip, 6 - skip) 395 | self.clear_status_flags(False) 396 | assert isinstance(report, AbsoluteReport) 397 | if read_buttons: 398 | report.buttons &= 0xF8 399 | report.buttons = data[0] & 7 400 | report.x = data[2 - skip] | ((data[4 - skip] & 0x0F) << 8) 401 | report.y = data[3 - skip] | ((data[4 - skip] & 0xF0) << 4) 402 | report.z = data[5 - skip] & 0x3F 403 | elif self._mode == PINNACLE_RELATIVE: # if in relative mode 404 | assert isinstance(report, RelativeReport) 405 | has_scroll = self._intellimouse 406 | read_buttons = bool(read_buttons) # enforce bool data type 407 | data = self._rap_read_bytes( 408 | _PACKET_BYTE_0 + (not read_buttons), 2 + has_scroll + read_buttons 409 | ) 410 | self.clear_status_flags(False) 411 | if read_buttons: 412 | report.buttons &= 0xF8 413 | report.buttons = data[0] & 7 414 | unpacked = struct.unpack("b" * (2 + has_scroll), data[read_buttons:]) 415 | report.x, report.y = unpacked[0:2] 416 | if len(unpacked) > 2: 417 | report.scroll = unpacked[2] 418 | 419 | def clear_status_flags(self, post_delay=True): 420 | """This function clears the "Data Ready" flag which is reflected with 421 | the ``dr_pin``. 422 | 423 | :param post_delay: If ``True``, then this function waits the recommended 50 424 | milliseconds before exiting. Only set this to ``False`` if the following 425 | instructions do not require access to the Pinnacle ASIC.""" 426 | self._rap_write(_STATUS, 0) 427 | if post_delay: 428 | time.sleep(0.00005) # per official examples from Cirque 429 | 430 | @property 431 | def allow_sleep(self) -> bool: 432 | """This attribute specifies if the Pinnacle ASIC is allowed to sleep 433 | after about 5 seconds of idle (no input event). 434 | 435 | Set this attribute to ``True`` if you want the Pinnacle ASIC to enter sleep (low 436 | power) mode after about 5 seconds of inactivity (does not apply to 437 | `PINNACLE_ANYMEAS` mode). While the touch controller is in sleep mode, if a 438 | touch event or button press is detected, the Pinnacle ASIC will take about 300 439 | milliseconds to wake up (does not include handling the touch event or button 440 | press data). 441 | """ 442 | return bool(self._rap_read(_SYS_CONFIG) & 4) 443 | 444 | @allow_sleep.setter 445 | def allow_sleep(self, is_enabled: bool): 446 | self._rap_write( 447 | _SYS_CONFIG, (self._rap_read(_SYS_CONFIG) & 0xFB) | (is_enabled << 2) 448 | ) 449 | 450 | @property 451 | def shutdown(self) -> bool: 452 | """This attribute controls power of the Pinnacle ASIC. ``True`` means powered 453 | down (AKA standby mode), and ``False`` means not powered down (Active, Idle, or 454 | Sleep mode). 455 | 456 | .. note:: 457 | The ASIC will take about 300 milliseconds to complete the transition 458 | from powered down mode to active mode. No touch events or button presses 459 | will be monitored while powered down. 460 | """ 461 | return bool(self._rap_read(_SYS_CONFIG) & 2) 462 | 463 | @shutdown.setter 464 | def shutdown(self, is_off: bool): 465 | self._rap_write( 466 | _SYS_CONFIG, (self._rap_read(_SYS_CONFIG) & 0xFD) | (is_off << 1) 467 | ) 468 | 469 | @property 470 | def sample_rate(self) -> int: 471 | """This attribute controls how many samples (of data) per second are reported. 472 | 473 | Valid values are ``100``, ``80``, ``60``, ``40``, ``20``, ``10``. Any other 474 | input values automatically set the sample rate to ``100`` sps (samples per 475 | second). 476 | 477 | Optionally (on older trackpads), ``200`` and ``300`` sps can be specified, but 478 | using these values automatically disables palm (referred to as "NERD" in the 479 | specification sheet) and noise compensations. These higher values are meant for 480 | using a stylus with a 2mm diameter tip, while the values less than 200 are meant 481 | for a finger or stylus with a 5.25mm diameter tip. 482 | 483 | .. warning:: The values ``200`` and ``300`` are |rev2025| Specifying these values on 484 | newer trackpads will be automatically clamped to ``100``. 485 | 486 | This attribute only applies to `PINNACLE_RELATIVE` or `PINNACLE_ABSOLUTE` mode. 487 | Otherwise if `data_mode` is set to `PINNACLE_ANYMEAS`, then this attribute will 488 | have no effect. 489 | """ 490 | return self._rap_read(_SAMPLE_RATE) 491 | 492 | @sample_rate.setter 493 | def sample_rate(self, val: int): 494 | if self._mode != PINNACLE_ANYMEAS: 495 | if val in (200, 300) and not self._rev2025: 496 | # disable palm & noise compensations 497 | self._rap_write(_FEED_CONFIG_3, 10) 498 | reload_timer = 6 if val == 300 else 0x09 499 | self._era_write_bytes(0x019E, reload_timer, 2) 500 | val = 0 501 | else: 502 | # enable palm & noise compensations 503 | self._rap_write(_FEED_CONFIG_3, 0) 504 | if not self._rev2025: 505 | self._era_write_bytes(0x019E, 0x13, 2) 506 | val = val if val in (100, 80, 60, 40, 20, 10) else 100 507 | self._rap_write(_SAMPLE_RATE, val) 508 | 509 | def detect_finger_stylus( 510 | self, 511 | enable_finger: bool = True, 512 | enable_stylus: bool = True, 513 | sample_rate: int = 100, 514 | ): 515 | """This function will configure the Pinnacle ASIC to detect either 516 | finger, stylus, or both. 517 | 518 | .. warning:: This method is |rev2025| Calling this method |rev2025-no-effect|. 519 | 520 | :param enable_finger: ``True`` enables the Pinnacle ASIC's measurements to 521 | detect if the touch event was caused by a finger or 5.25 mm stylus. 522 | ``False`` disables this feature. Default is ``True``. 523 | :param enable_stylus: ``True`` enables the Pinnacle ASIC's measurements to 524 | detect if the touch event was caused by a 2 mm stylus. ``False`` disables 525 | this feature. Default is ``True``. 526 | :param sample_rate: See the `sample_rate` attribute as this parameter 527 | manipulates that attribute. 528 | 529 | .. tip:: 530 | Consider adjusting the ADC matrix's gain to enhance performance/results 531 | using `set_adc_gain()` 532 | """ 533 | if self._rev2025: 534 | return 535 | finger_stylus = self._era_read(0x00EB) 536 | finger_stylus |= (enable_stylus << 2) | enable_finger 537 | self._era_write(0x00EB, finger_stylus) 538 | self.sample_rate = sample_rate 539 | 540 | def calibrate( 541 | self, 542 | run: bool = True, 543 | tap: bool = True, 544 | track_error: bool = True, 545 | nerd: bool = True, 546 | background: bool = True, 547 | ) -> bool: 548 | """Set calibration parameters when the Pinnacle ASIC calibrates 549 | itself. 550 | 551 | This function only applies to `PINNACLE_RELATIVE` or `PINNACLE_ABSOLUTE` mode. 552 | Otherwise if `data_mode` is set to `PINNACLE_ANYMEAS`, then this function will 553 | have no effect. 554 | 555 | :param run: If ``True``, this function forces a calibration of the sensor. If 556 | ``False``, this function just writes the following parameters to the 557 | Pinnacle ASIC's "CalConfig1" register. 558 | :param tap: Enable dynamic tap compensation? Default is ``True``. 559 | :param track_error: Enable dynamic track error compensation? Default is 560 | ``True``. 561 | :param nerd: Enable dynamic NERD compensation? Default is ``True``. This 562 | parameter has something to do with palm detection/compensation. 563 | :param background: Enable dynamic background compensation? Default is ``True``. 564 | 565 | :Returns: 566 | ``False`` 567 | - If `data_mode` is not set to `PINNACLE_RELATIVE` or 568 | `PINNACLE_ABSOLUTE`. 569 | - If the calibration ``run`` timed out after 100 milliseconds. 570 | ``True`` 571 | - If `data_mode` is set to `PINNACLE_RELATIVE` or `PINNACLE_ABSOLUTE` 572 | and the calibration is **not** ``run``. 573 | - If the calibration ``run`` successfully finishes. 574 | """ 575 | if self._mode not in (PINNACLE_RELATIVE, PINNACLE_ABSOLUTE): 576 | return False 577 | cal_config = (tap << 4) | (track_error << 3) | (nerd << 2) 578 | cal_config |= background << 1 579 | self._rap_write(_CAL_CONFIG, cal_config | run) 580 | timeout = time.monotonic_ns() + 100000000 581 | if run: 582 | done = False 583 | while not done and time.monotonic_ns() < timeout: 584 | done = self.available() # calibration is running 585 | if done: 586 | self.clear_status_flags() # now that calibration is done 587 | return done 588 | return True 589 | 590 | @property 591 | def calibration_matrix(self) -> List[int]: 592 | """This attribute returns a `list` of the 46 signed 16-bit (short) 593 | values stored in the Pinnacle ASIC's memory that is used for taking 594 | measurements. 595 | 596 | .. warning:: This attribute is |rev2025| Using this attribute 597 | |rev2025-no-effect| and return an empty `list`. 598 | 599 | This matrix is not applicable in AnyMeas mode. Use this attribute to compare a 600 | prior compensation matrix with a new matrix that was either loaded manually by 601 | setting this attribute to a `list` of 46 signed 16-bit (short) integers or 602 | created internally by calling `calibrate()` with the ``run`` parameter as 603 | ``True``. 604 | 605 | .. note:: 606 | A paraphrased note from Cirque's Application Note on Comparing compensation 607 | matrices: 608 | 609 | If any 16-bit values are above 20K (absolute), it generally indicates a 610 | problem with the sensor. If no values exceed 20K, proceed with the data 611 | comparison. Compare each 16-bit value in one matrix to the corresponding 612 | 16-bit value in the other matrix. If the difference between the two values 613 | is greater than 500 (absolute), it indicates a change in the environment. 614 | Either an object was on the sensor during calibration, or the surrounding 615 | conditions (temperature, humidity, or noise level) have changed. One 616 | strategy is to force another calibration and compare again, if the values 617 | continue to differ by 500, determine whether to use the new data or a 618 | previous set of stored data. Another strategy is to average any two values 619 | that differ by more than 500 and write this new matrix, with the average 620 | values, back into Pinnacle ASIC. 621 | """ 622 | if self._rev2025: 623 | return [] 624 | # combine every 2 bytes from resulting buffer into list of signed 625 | # 16-bits integers 626 | return list(struct.unpack("46h", self._era_read_bytes(0x01DF, 92))) 627 | 628 | @calibration_matrix.setter 629 | def calibration_matrix(self, matrix: List[int]): 630 | if self._rev2025: 631 | return 632 | matrix += [0] * (46 - len(matrix)) # pad short matrices w/ 0s 633 | for index in range(46): 634 | buf = struct.pack("h", matrix[index]) 635 | self._era_write(0x01DF + index * 2, buf[0]) 636 | self._era_write(0x01DF + index * 2 + 1, buf[1]) 637 | 638 | def set_adc_gain(self, sensitivity: int): 639 | """Sets the ADC gain in range [0, 3] to enhance performance based on 640 | the overlay type (does not apply to AnyMeas mode). 641 | 642 | .. warning:: This method is |rev2025| Calling this method |rev2025-no-effect|. 643 | 644 | :param sensitivity: Specifies how sensitive the ADC (Analog to Digital 645 | Converter) component is. ``0`` means most sensitive, and ``3`` means least 646 | sensitive. A value outside this range will raise a `ValueError` exception. 647 | 648 | .. tip:: 649 | The official example code from Cirque for a curved overlay uses a value 650 | of ``1``. 651 | """ 652 | if self._rev2025: 653 | return 654 | if not 0 <= sensitivity < 4: 655 | raise ValueError("sensitivity is out of bounds [0,3]") 656 | val = self._era_read(0x0187) & 0x3F | (sensitivity << 6) 657 | self._era_write(0x0187, val) 658 | 659 | def tune_edge_sensitivity( 660 | self, x_axis_wide_z_min: int = 0x04, y_axis_wide_z_min: int = 0x03 661 | ): 662 | """Changes thresholds to improve detection of fingers. 663 | 664 | .. warning:: This method is |rev2025| Calling this method |rev2025-no-effect|. 665 | 666 | .. warning:: 667 | This function was ported from Cirque's example code and doesn't seem to have 668 | corresponding documentation. This function directly alters values in the 669 | Pinnacle ASIC's memory. USE AT YOUR OWN RISK! 670 | """ 671 | if self._rev2025: 672 | return 673 | self._era_write(0x0149, x_axis_wide_z_min) 674 | self._era_write(0x0168, y_axis_wide_z_min) 675 | 676 | def anymeas_mode_config( 677 | self, 678 | gain: int = PINNACLE_GAIN_200, 679 | frequency: int = PINNACLE_FREQ_0, 680 | sample_length: int = 512, 681 | mux_ctrl: int = PINNACLE_MUX_PNP, 682 | aperture_width: int = 500, 683 | ctrl_pwr_cnt: int = 1, 684 | ): 685 | """This function configures the Pinnacle ASIC to output raw ADC 686 | measurements. 687 | 688 | Be sure to set the `data_mode` attribute to `PINNACLE_ANYMEAS` before calling 689 | this function, otherwise it will do nothing. 690 | 691 | :param gain: Sets the sensitivity of the ADC matrix. Valid values are the 692 | constants defined in `AnyMeas mode Gain`_. Defaults to `PINNACLE_GAIN_200`. 693 | :param frequency: Sets the frequency of measurements made by the ADC matrix. 694 | Valid values are the constants defined in `AnyMeas mode Frequencies`_. 695 | Defaults to `PINNACLE_FREQ_0`. 696 | :param sample_length: Sets the maximum bit length of the measurements made by 697 | the ADC matrix. Valid values are ``128``, ``256``, or ``512``. Defaults to 698 | ``512``. 699 | :param mux_ctrl: The Pinnacle ASIC can employ different bipolar junctions 700 | and/or reference capacitors. Valid values are the constants defined in 701 | `AnyMeas mode Muxing`_. Additional combination of these constants is also 702 | allowed. Defaults to `PINNACLE_MUX_PNP`. 703 | :param aperture_width: Sets the window of time (in nanoseconds) to allow for 704 | the ADC to take a measurement. Valid values are multiples of 125 in range 705 | [``250``, ``1875``]. Erroneous values are clamped/truncated to this range. 706 | 707 | .. note:: The ``aperture_width`` parameter has a inverse 708 | relationship/affect on the ``frequency`` parameter. The approximated 709 | frequencies described in this documentation are based on an aperture 710 | width of 500 nanoseconds, and they will shrink as the aperture width 711 | grows or grow as the aperture width shrinks. 712 | 713 | :param ctrl_pwr_cnt: Configure the Pinnacle to perform a number of measurements 714 | for each call to `measure_adc()`. Defaults to 1. Constants defined in 715 | `AnyMeas mode Control`_ can be used to specify if is sleep is allowed 716 | (`PINNACLE_CTRL_PWR_IDLE` -- this is not default) or if repetitive 717 | measurements is allowed (`PINNACLE_CTRL_REPEAT`) if number of measurements 718 | is more than 1. 719 | 720 | .. warning:: 721 | There is no bounds checking on the number of measurements specified 722 | here. Specifying more than 63 will trigger sleep mode after performing 723 | measurements. 724 | 725 | .. tip:: 726 | Be aware that allowing the Pinnacle to enter sleep mode after taking 727 | measurements will slow consecutive calls to `measure_adc()` as the 728 | Pinnacle requires about 300 milliseconds to wake up. 729 | """ 730 | if self._mode == PINNACLE_ANYMEAS: 731 | buffer = bytearray(10) 732 | buffer[0] = gain | frequency 733 | buffer[1] = max(1, min(int(sample_length / 128), 3)) 734 | buffer[2] = mux_ctrl 735 | buffer[4] = max(2, min(int(aperture_width / 125), 15)) 736 | buffer[6] = _PACKET_BYTE_1 737 | buffer[9] = ctrl_pwr_cnt 738 | self._rap_write_bytes(_FEED_CONFIG_2, buffer) 739 | self._rap_write_bytes(_PACKET_BYTE_1, bytes(8)) 740 | self.clear_status_flags() 741 | 742 | def measure_adc(self, bits_to_toggle: int, toggle_polarity: int) -> Optional[int]: 743 | """This blocking function instigates and returns the measurements (a 744 | signed short) from the Pinnacle ASIC's ADC (Analog to Digital Converter) matrix. 745 | 746 | Internally this function calls `start_measure_adc()` and `get_measure_adc()` in 747 | sequence. Be sure to set the `data_mode` attribute to `PINNACLE_ANYMEAS` before 748 | calling this function otherwise it will do nothing. 749 | 750 | Each of the parameters are a 4-byte integer (see 751 | :ref:`format table below `) in which each bit corresponds to 752 | a capacitance sensing electrode in the sensor's matrix (12 electrodes for 753 | Y-axis, 16 electrodes for X-axis). They are used to compensate for varying 754 | capacitances in the electrodes during measurements. 755 | 756 | :param bits_to_toggle: A bit of ``1`` flags that electrode's output for 757 | toggling, and a bit of ``0`` signifies that the electrode's output should 758 | remain unaffected. 759 | :param toggle_polarity: This specifies which polarity the output of the 760 | electrode(s) (specified with corresponding bits in ``bits_to_toggle`` 761 | parameter) should be toggled (forced). A bit of ``1`` toggles that bit 762 | positive, and a bit of ``0`` toggles that bit negative. 763 | 764 | :Returns: 765 | A 2-byte `bytearray` that represents a signed short integer. If `data_mode` 766 | is not set to `PINNACLE_ANYMEAS`, then this function returns `None` and does 767 | nothing. 768 | 769 | .. _polynomial-fmt: 770 | 771 | :4-byte Integer Format: 772 | Bits 31 & 30 are not used and should remain ``0``. Bits 29 and 28 represent 773 | the optional implementation of reference capacitors built into the Pinnacle 774 | ASIC. To use these capacitors, the corresponding constants 775 | (`PINNACLE_MUX_REF0` and/or `PINNACLE_MUX_REF1`) must be passed to 776 | `anymeas_mode_config()` in the ``mux_ctrl`` parameter, and their 777 | representative bits must be flagged in both ``bits_to_toggle`` & 778 | ``toggle_polarity`` parameters. 779 | 780 | .. csv-table:: byte 3 (MSByte) 781 | :stub-columns: 1 782 | :widths: 10, 5, 5, 5, 5, 5, 5, 5, 5 783 | 784 | "bit position",31,30,29,28,27,26,25,24 785 | "representation",N/A,N/A,Ref1,Ref0,Y11,Y10,Y9,Y8 786 | .. csv-table:: byte 2 787 | :stub-columns: 1 788 | :widths: 10, 5, 5, 5, 5, 5, 5, 5, 5 789 | 790 | "bit position",23,22,21,20,19,18,17,16 791 | "representation",Y7,Y6,Y5,Y4,Y3,Y2,Y1,Y0 792 | .. csv-table:: byte 1 793 | :stub-columns: 1 794 | :widths: 10, 5, 5, 5, 5, 5, 5, 5, 5 795 | 796 | "bit position",15,14,13,12,11,10,9,8 797 | "representation",X15,X14,X13,X12,X11,X10,X9,X8 798 | .. csv-table:: byte 0 (LSByte) 799 | :stub-columns: 1 800 | :widths: 10, 5, 5, 5, 5, 5, 5, 5, 5 801 | 802 | "bit position",7,6,5,4,3,2,1,0 803 | "representation",X7,X6,X5,X4,X3,X2,X1,X0 804 | 805 | .. seealso:: 806 | Review `AnyMeas mode example `_ to 807 | understand how to use these 4-byte integers. 808 | """ 809 | if self._mode != PINNACLE_ANYMEAS: 810 | return None 811 | self.start_measure_adc(bits_to_toggle, toggle_polarity) 812 | while not self.available(): 813 | pass # wait till measurements are complete 814 | return self.get_measure_adc() 815 | 816 | def start_measure_adc(self, bits_to_toggle: int, toggle_polarity: int): 817 | """A non-blocking function that starts measuring ADC values in 818 | AnyMeas mode. 819 | 820 | See the parameters and table in `measure_adc()` as this is its helper function, 821 | and all parameters there are used the same way here. 822 | """ 823 | if self._mode == PINNACLE_ANYMEAS: 824 | tog_pol = bytearray(8) # assemble list of register buffers 825 | for i in range(3, -1, -1): 826 | tog_pol[3 - i] = (bits_to_toggle >> (i * 8)) & 0xFF 827 | tog_pol[3 - i + 4] = (toggle_polarity >> (i * 8)) & 0xFF 828 | # write toggle and polarity parameters to register 0x13 - 0x1A 829 | self._rap_write_bytes(_PACKET_BYTE_1, tog_pol) 830 | # clear_status_flags() and initiate measurements 831 | self._rap_write_bytes(_STATUS, b"\0\x18") 832 | 833 | def get_measure_adc(self) -> Optional[int]: 834 | """A non-blocking function that returns ADC measurement on 835 | completion. 836 | 837 | This function is only meant to be used in conjunction with `start_measure_adc()` 838 | for non-blocking application. Be sure that `available()` returns ``True`` before 839 | calling this function as it will `clear_status_flags()` that `available()` uses. 840 | 841 | :returns: 842 | * `None` if `data_mode` is not set to `PINNACLE_ANYMEAS` or if the "data 843 | ready" pin's signal is not active (while `data_mode` is set to 844 | `PINNACLE_ANYMEAS`) meaning the Pinnacle ASIC is still computing the ADC 845 | measurements based on the 4-byte polynomials passed to 846 | `start_measure_adc()`. 847 | * a `bytearray` that represents a signed 16-bit integer upon completed ADC 848 | measurements based on the 4-byte polynomials passed to 849 | `start_measure_adc()`. 850 | """ 851 | if self._mode != PINNACLE_ANYMEAS: 852 | return None 853 | data = self._rap_read_bytes(0x11, 2) 854 | self.clear_status_flags() 855 | return struct.unpack("h", data)[0] 856 | 857 | def _rap_read(self, reg: int) -> int: 858 | raise NotImplementedError() 859 | 860 | def _rap_read_bytes(self, reg: int, numb_bytes: int) -> bytearray: 861 | raise NotImplementedError() 862 | 863 | def _rap_write(self, reg: int, value: int): 864 | raise NotImplementedError() 865 | 866 | def _rap_write_cmd(self, cmd: bytes): 867 | raise NotImplementedError() 868 | 869 | def _rap_write_bytes(self, reg: int, values: Iterable[int]): 870 | raise NotImplementedError() 871 | 872 | def _era_read(self, reg: int) -> int: 873 | prev_feed_state = self.feed_enable 874 | if prev_feed_state: 875 | self.feed_enable = False # accessing raw memory, so do this 876 | if self._rev2025: 877 | self.clear_status_flags() 878 | self._rap_write_bytes(_ERA_ADDR, bytes([reg >> 8, reg & 0xFF])) 879 | self._rap_write(_ERA_CONTROL, 1) # indicate reading only 1 byte 880 | if self._rev2025: 881 | while not self.dr_pin.value: # wait for command to complete 882 | pass 883 | else: 884 | while self._rap_read(_ERA_CONTROL): # read until reg == 0 885 | pass # also sets Command Complete flag in Status register 886 | buf = self._rap_read(_ERA_VALUE) # get value 887 | self.clear_status_flags() 888 | if prev_feed_state: 889 | self.feed_enable = prev_feed_state # resume previous feed state 890 | return buf 891 | 892 | def _era_read_bytes(self, reg: int, numb_bytes: int) -> bytes: 893 | buf = b"" 894 | prev_feed_state = self.feed_enable 895 | if prev_feed_state: 896 | self.feed_enable = False # accessing raw memory, so do this 897 | if self._rev2025: 898 | self.clear_status_flags() 899 | self._rap_write_bytes(_ERA_ADDR, bytes([reg >> 8, reg & 0xFF])) 900 | for _ in range(numb_bytes): 901 | self._rap_write(_ERA_CONTROL, 5) # indicate reading sequential bytes 902 | if self._rev2025: 903 | while not self.dr_pin.value: # wait for command to complete 904 | pass 905 | else: 906 | while self._rap_read(_ERA_CONTROL): # read until reg == 0 907 | pass # also sets Command Complete flag in Status register 908 | buf += bytes([self._rap_read(_ERA_VALUE)]) # get value 909 | self.clear_status_flags() 910 | if prev_feed_state: 911 | self.feed_enable = prev_feed_state # resume previous feed state 912 | return buf 913 | 914 | def _era_write(self, reg: int, value: int): 915 | prev_feed_state = self.feed_enable 916 | if prev_feed_state: 917 | self.feed_enable = False # accessing raw memory, so do this 918 | if self._rev2025: 919 | self.clear_status_flags() 920 | self._rap_write(_ERA_VALUE, value) # write value 921 | self._rap_write_bytes(_ERA_ADDR, bytes([reg >> 8, reg & 0xFF])) 922 | self._rap_write(_ERA_CONTROL, 2) # indicate writing only 1 byte 923 | if self._rev2025: 924 | while not self.dr_pin.value: # wait for command to complete 925 | pass 926 | else: 927 | while self._rap_read(_ERA_CONTROL): # read until reg == 0 928 | pass # also sets Command Complete flag in Status register 929 | self.clear_status_flags() 930 | if prev_feed_state: 931 | self.feed_enable = prev_feed_state # resume previous feed state 932 | 933 | def _era_write_bytes(self, reg: int, value: int, numb_bytes: int): 934 | # rarely used as it only writes 1 value to multiple registers 935 | prev_feed_state = self.feed_enable 936 | if prev_feed_state: 937 | self.feed_enable = False # accessing raw memory, so do this 938 | if self._rev2025: 939 | self.clear_status_flags() 940 | self._rap_write(_ERA_VALUE, value) # write value 941 | self._rap_write_bytes(_ERA_ADDR, bytes([reg >> 8, reg & 0xFF])) 942 | self._rap_write(_ERA_CONTROL, 0x0A) # indicate writing sequential bytes 943 | for _ in range(numb_bytes): 944 | if self._rev2025: 945 | while not self.dr_pin.value: # wait for command to complete 946 | pass 947 | else: 948 | while self._rap_read(_ERA_CONTROL): # read until reg == 0 949 | pass # also sets Command Complete flag in Status register 950 | self.clear_status_flags() 951 | if prev_feed_state: 952 | self.feed_enable = prev_feed_state # resume previous feed state 953 | 954 | 955 | # pylint: disable=no-member 956 | class PinnacleTouchI2C(PinnacleTouch): 957 | """A derived class for interfacing with the Pinnacle ASIC via the I2C protocol. 958 | 959 | :param i2c: The object of the I2C bus to use. This object must be shared among other 960 | driver classes that use the same I2C bus (SDA & SCL pins). 961 | :param dr_pin: |dr_pin_parameter| 962 | 963 | .. versionchanged:: 2.0.0 ``dr_pin`` is a required parameter. 964 | 965 | |dr_pin_required| 966 | :param address: The slave I2C address of the Pinnacle ASIC. Defaults to ``0x2A``. 967 | """ 968 | 969 | def __init__( 970 | self, 971 | i2c: busio.I2C, 972 | dr_pin: digitalio.DigitalInOut, 973 | address: int = 0x2A, 974 | ): 975 | self._i2c = I2CDevice(i2c, address) 976 | super().__init__(dr_pin=dr_pin) 977 | 978 | def _rap_read(self, reg: int) -> int: 979 | return self._rap_read_bytes(reg, 1)[0] 980 | 981 | def _rap_read_bytes(self, reg: int, numb_bytes: int) -> bytearray: 982 | buf = bytes([reg | 0xA0]) # per datasheet 983 | with self._i2c as i2c: 984 | i2c.write(buf) # includes a STOP condition 985 | buf = bytearray(numb_bytes) # for response(s) 986 | # auto-increments register for each byte read 987 | i2c.readinto(buf) 988 | return buf 989 | 990 | def _rap_write(self, reg: int, value: int): 991 | self._rap_write_bytes(reg, bytes([value])) 992 | 993 | def _rap_write_bytes(self, reg: int, values: Iterable[int]): 994 | buf = b"" 995 | for index, byte in enumerate(values): 996 | # Pinnacle doesn't auto-increment register 997 | # addresses for I2C write operations 998 | buf += bytes([(reg + index) | 0x80, byte & 0xFF]) 999 | self._rap_write_cmd(buf) 1000 | 1001 | def _rap_write_cmd(self, cmd: bytes): 1002 | with self._i2c as i2c: 1003 | i2c.write(cmd) 1004 | 1005 | 1006 | class PinnacleTouchSPI(PinnacleTouch): 1007 | """A derived class for interfacing with the Pinnacle ASIC via the SPI protocol. 1008 | 1009 | :param spi: The object of the SPI bus to use. This object must be shared among other 1010 | driver classes that use the same SPI bus (MOSI, MISO, & SCK pins). 1011 | :param ss_pin: The "slave select" pin output to the Pinnacle ASIC. 1012 | :param dr_pin: |dr_pin_parameter| 1013 | 1014 | .. versionchanged:: 2.0.0 ``dr_pin`` is a required parameter. 1015 | 1016 | |dr_pin_required| 1017 | :param spi_frequency: The SPI bus speed in Hz. Default is the maximum 13 MHz. 1018 | """ 1019 | 1020 | def __init__( 1021 | self, 1022 | spi: busio.SPI, 1023 | ss_pin: digitalio.DigitalInOut, 1024 | dr_pin: digitalio.DigitalInOut, 1025 | spi_frequency: int = 13000000, 1026 | ): 1027 | self._spi = SPIDevice(spi, chip_select=ss_pin, phase=1, baudrate=spi_frequency) 1028 | super().__init__(dr_pin=dr_pin) 1029 | 1030 | def _rap_read(self, reg: int) -> int: 1031 | buf_out = bytes([reg | 0xA0]) + b"\xfb" * 3 1032 | buf_in = bytearray(len(buf_out)) 1033 | with self._spi as spi: 1034 | spi.write_readinto(buf_out, buf_in) 1035 | return buf_in[3] 1036 | 1037 | def _rap_read_bytes(self, reg: int, numb_bytes: int) -> bytearray: 1038 | # using auto-increment method 1039 | buf_out = bytes([reg | 0xA0]) + b"\xfc" * (1 + numb_bytes) + b"\xfb" 1040 | buf_in = bytearray(len(buf_out)) 1041 | with self._spi as spi: 1042 | spi.write_readinto(buf_out, buf_in) 1043 | return buf_in[3:] 1044 | 1045 | def _rap_write_cmd(self, cmd: bytes): 1046 | with self._spi as spi: 1047 | spi.write(cmd) 1048 | 1049 | def _rap_write(self, reg: int, value: int): 1050 | self._rap_write_cmd(bytes([(reg | 0x80), value])) 1051 | 1052 | def _rap_write_bytes(self, reg: int, values: Iterable[int]): 1053 | for i, val in enumerate(values): 1054 | self._rap_write(reg + i, val) 1055 | -------------------------------------------------------------------------------- /cspell.config.yml: -------------------------------------------------------------------------------- 1 | version: "0.2" 2 | words: 3 | - adafruit 4 | - autoattribute 5 | - autoclass 6 | - automethod 7 | - ANYMEAS 8 | - ASIC 9 | - baudrate 10 | - busio 11 | - circuitpython 12 | - datasheet 13 | - digitalio 14 | - intellimouse 15 | - micropython 16 | - MOSI 17 | - Muxing 18 | - pipx 19 | - seealso 20 | - sparkfun 21 | - tolower 22 | - trackpad 23 | - trackpads 24 | -------------------------------------------------------------------------------- /docs/_static/Cirque_GlidePoint-Circle-Trackpad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bndy5/CircuitPython_Cirque_Pinnacle/9352800c9a1a7dac336060a8bc4153c039fe7bed/docs/_static/Cirque_GlidePoint-Circle-Trackpad.png -------------------------------------------------------------------------------- /docs/_static/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bndy5/CircuitPython_Cirque_Pinnacle/9352800c9a1a7dac336060a8bc4153c039fe7bed/docs/_static/Logo.png -------------------------------------------------------------------------------- /docs/_static/extra_css.css: -------------------------------------------------------------------------------- 1 | 2 | th { 3 | background-color: var(--md-default-fg-color--lightest); 4 | } 5 | 6 | .md-nav.md-nav--primary > label { 7 | white-space: normal; 8 | line-height: inherit; 9 | padding-top: 3.5rem; 10 | } 11 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2bndy5/CircuitPython_Cirque_Pinnacle/9352800c9a1a7dac336060a8bc4153c039fe7bed/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/anymeas.rst: -------------------------------------------------------------------------------- 1 | 2 | AnyMeas mode API 3 | ================ 4 | 5 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.anymeas_mode_config 6 | 7 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.measure_adc 8 | 9 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.start_measure_adc 10 | 11 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.get_measure_adc 12 | 13 | AnyMeas mode Gain 14 | ----------------- 15 | 16 | Allowed ADC gain configurations of AnyMeas mode. The percentages defined here are approximate 17 | values. 18 | 19 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_GAIN_100 20 | :no-value: 21 | 22 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_GAIN_133 23 | :no-value: 24 | 25 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_GAIN_166 26 | :no-value: 27 | 28 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_GAIN_200 29 | :no-value: 30 | 31 | AnyMeas mode Frequencies 32 | ------------------------ 33 | 34 | Allowed frequency configurations of AnyMeas mode. The frequencies defined here are 35 | approximated based on an aperture width of 500 nanoseconds. If the ``aperture_width`` 36 | parameter to `anymeas_mode_config()` specified is less than 500 nanoseconds, then the 37 | frequency will be larger than what is described here (& vice versa). 38 | 39 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_FREQ_0 40 | :no-value: 41 | 42 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_FREQ_1 43 | :no-value: 44 | 45 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_FREQ_2 46 | :no-value: 47 | 48 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_FREQ_3 49 | :no-value: 50 | 51 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_FREQ_4 52 | :no-value: 53 | 54 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_FREQ_5 55 | :no-value: 56 | 57 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_FREQ_6 58 | :no-value: 59 | 60 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_FREQ_7 61 | :no-value: 62 | 63 | AnyMeas mode Muxing 64 | ------------------- 65 | 66 | Allowed muxing gate polarity and reference capacitor configurations of AnyMeas mode. 67 | Combining these values (with ``+`` operator) is allowed. 68 | 69 | .. note:: 70 | The sign of the measurements taken in AnyMeas mode is inverted depending on which 71 | muxing gate is specified (when specifying an individual gate polarity). 72 | 73 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_MUX_REF1 74 | :no-value: 75 | 76 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_MUX_REF0 77 | :no-value: 78 | 79 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_MUX_PNP 80 | :no-value: 81 | 82 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_MUX_NPN 83 | :no-value: 84 | 85 | AnyMeas mode Control 86 | -------------------- 87 | 88 | These constants control the number of measurements performed in `measure_adc()`. 89 | The number of measurements can range [0, 63]. 90 | 91 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_CTRL_REPEAT 92 | :no-value: 93 | 94 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_CTRL_PWR_IDLE 95 | :no-value: 96 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | PinnacleTouch API 3 | ================== 4 | 5 | Data Modes 6 | ---------- 7 | 8 | Allowed symbols for configuring the Pinnacle ASIC's data reporting/measurements. 9 | These are used as valid values for `PinnacleTouch.data_mode`. 10 | 11 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_RELATIVE 12 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_ANYMEAS 13 | .. autodata:: circuitpython_cirque_pinnacle.PINNACLE_ABSOLUTE 14 | 15 | PinnacleTouch class 16 | ------------------- 17 | 18 | .. autoclass:: circuitpython_cirque_pinnacle.PinnacleTouch 19 | :no-members: 20 | 21 | .. autoattribute:: circuitpython_cirque_pinnacle.PinnacleTouch.data_mode 22 | .. autoattribute:: circuitpython_cirque_pinnacle.PinnacleTouch.rev2025 23 | 24 | SPI & I2C Interfaces 25 | -------------------- 26 | 27 | .. autoclass:: circuitpython_cirque_pinnacle.PinnacleTouchSPI 28 | :members: 29 | :show-inheritance: 30 | 31 | .. autoclass:: circuitpython_cirque_pinnacle.PinnacleTouchI2C 32 | :members: 33 | :show-inheritance: 34 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name,too-few-public-methods 2 | """This file is for `sphinx-build` configuration""" 3 | 4 | import os 5 | import sys 6 | 7 | 8 | sys.path.insert(0, os.path.abspath("..")) 9 | 10 | # -- General configuration 11 | # ------------------------------------------------ 12 | 13 | # Add any Sphinx extension module names here, as strings. They can be 14 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 15 | # ones. 16 | extensions = [ 17 | "sphinx.ext.autodoc", 18 | "sphinx.ext.intersphinx", 19 | "sphinx.ext.napoleon", 20 | "sphinx.ext.todo", 21 | "sphinx.ext.viewcode", 22 | "sphinx_immaterial", 23 | # "rst2pdf.pdfbuilder", # for local pdf builder support 24 | ] 25 | 26 | # Uncomment the below if you use native CircuitPython modules such as 27 | # digitalio, micropython and busio. List the modules you use. Without it, the 28 | # autodoc module docs will fail to generate with a warning. 29 | # autodoc_mock_imports = ["digitalio", "busio", "usb_hid", "microcontroller"] 30 | autodoc_member_order = "bysource" 31 | 32 | intersphinx_mapping = { 33 | "python": ("https://docs.python.org/3", None), 34 | "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None), 35 | } 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | source_suffix = ".rst" 41 | 42 | # The master toctree document. 43 | master_doc = "index" 44 | 45 | # General information about the project. 46 | project = "Circuitpython Cirque Pinnacle Library" 47 | copyright = "2020 Brendan Doherty" # pylint: disable=redefined-builtin 48 | author = "Brendan Doherty" 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | version = "dev" 56 | # The full version, including alpha/beta/rc tags. 57 | release = "1.0" 58 | 59 | html_baseurl = "https://circuitpython-cirque-pinnacle.readthedocs.io/" 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = "en" 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This patterns also effect to html_static_path and html_extra_path 71 | exclude_patterns = [ 72 | "_build", 73 | "Thumbs.db", 74 | ".DS_Store", 75 | ".env", 76 | "CODE_OF_CONDUCT.md", 77 | "requirements.txt", 78 | ] 79 | 80 | # The reST default role (used for this markup: `text`) to use for all 81 | # documents. 82 | # 83 | default_role = "any" 84 | 85 | rst_prolog = """ 86 | .. role:: python(code) 87 | :language: python 88 | :class: highlight 89 | .. default-literal-role:: python 90 | 91 | .. |rev2025| replace:: not supported on trackpads manufactured on or after 2025. 92 | Defer to :py:attr:`~circuitpython_cirque_pinnacle.PinnacleTouch.rev2025`. 93 | .. |rev2025-no-effect| replace:: on newer trackpads will have no effect 94 | """ 95 | 96 | # If true, '()' will be appended to :func: etc. cross-reference text. 97 | # 98 | add_function_parentheses = True 99 | 100 | # If true, `todo` and `todoList` produce output, else they produce nothing. 101 | todo_include_todos = False 102 | 103 | # If this is True, todo emits a warning for each TODO entries. The default is False. 104 | todo_emit_warnings = False 105 | 106 | napoleon_numpy_docstring = False 107 | 108 | # -- Options for HTML output ---------------------------------------------- 109 | 110 | # The theme to use for HTML and HTML Help pages. See the documentation for 111 | # a list of builtin themes. 112 | html_theme = "sphinx_immaterial" 113 | 114 | html_theme_options = { 115 | "features": [ 116 | "search.share", 117 | ], 118 | # Set the color and the accent color 119 | "palette": [ 120 | { 121 | "media": "(prefers-color-scheme)", 122 | "toggle": { 123 | "icon": "material/brightness-auto", 124 | "name": "Switch to light mode", 125 | }, 126 | }, 127 | { 128 | "media": "(prefers-color-scheme: light)", 129 | "scheme": "default", 130 | "primary": "green", 131 | "accent": "light-blue", 132 | "toggle": { 133 | "icon": "material/lightbulb-outline", 134 | "name": "Switch to dark mode", 135 | }, 136 | }, 137 | { 138 | "media": "(prefers-color-scheme: dark)", 139 | "scheme": "slate", 140 | "primary": "green", 141 | "accent": "light-blue", 142 | "toggle": { 143 | "icon": "material/lightbulb", 144 | "name": "Switch to light mode", 145 | }, 146 | }, 147 | ], 148 | # Set the repo location to get a badge with stats 149 | "repo_url": "https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle/", 150 | "repo_name": "CircuitPython_Cirque_Pinnacle", 151 | "social": [ 152 | { 153 | "icon": "fontawesome/brands/github", 154 | "link": "https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle", 155 | }, 156 | { 157 | "icon": "fontawesome/brands/python", 158 | "link": "https://pypi.org/project/circuitpython-cirque-pinnacle/", 159 | }, 160 | { 161 | "icon": "fontawesome/brands/discord", 162 | "link": "https://adafru.it/discord", 163 | }, 164 | { 165 | "icon": "simple/adafruit", 166 | "link": "https://www.adafruit.com/", 167 | }, 168 | { 169 | "icon": "simple/sparkfun", 170 | "link": "https://www.sparkfun.com/", 171 | }, 172 | { 173 | "name": "CircuitPython Downloads", 174 | "icon": "octicons/download-24", 175 | "link": "https://circuitpython.org", 176 | }, 177 | ], 178 | } 179 | 180 | sphinx_immaterial_custom_admonitions = [ 181 | { 182 | "name": "warning", 183 | "color": (255, 66, 66), 184 | "icon": "octicons/alert-24", 185 | "override": True, 186 | }, 187 | { 188 | "name": "note", 189 | "icon": "octicons/pencil-24", 190 | "override": True, 191 | }, 192 | { 193 | "name": "seealso", 194 | "color": (255, 66, 252), 195 | "icon": "octicons/eye-24", 196 | "title": "See Also", 197 | "override": True, 198 | }, 199 | { 200 | "name": "hint", 201 | "icon": "material/school", 202 | "override": True, 203 | }, 204 | { 205 | "name": "tip", 206 | "icon": "material/school", 207 | "override": True, 208 | }, 209 | { 210 | "name": "important", 211 | "icon": "material/school", 212 | "override": True, 213 | }, 214 | ] 215 | 216 | python_type_aliases = { 217 | "DigitalInOut": "digitalio.DigitalInOut", 218 | } 219 | 220 | object_description_options = [ 221 | ("py:.*", dict(generate_synopses="first_sentence")), 222 | ] 223 | 224 | # Set link name generated in the top bar. 225 | html_title = "CircuitPython Cirque Pinnacle" 226 | 227 | # Add any paths that contain custom static files (such as style sheets) here, 228 | # relative to this directory. They are copied after the builtin static files, 229 | # so a file named "default.css" will overwrite the builtin "default.css". 230 | html_static_path = ["_static"] 231 | 232 | # These paths are either relative to html_static_path 233 | # or fully qualified paths (eg. https://...) 234 | html_css_files = ["extra_css.css"] 235 | 236 | # The name of an image file (relative to this directory) to use as a favicon of 237 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 238 | # pixels large. 239 | html_favicon = "_static/favicon.ico" 240 | 241 | html_logo = "_static/Logo.png" 242 | # Output file base name for HTML help builder. 243 | htmlhelp_basename = "CircuitpythonCirquePinnacleLibrarydoc" 244 | 245 | # -- Options for LaTeX output 246 | # --------------------------------------------- 247 | 248 | latex_elements = { 249 | # The paper size ('letterpaper' or 'a4paper'). 250 | "papersize": "letterpaper", 251 | # The font size ('10pt', '11pt' or '12pt'). 252 | "pointsize": "10pt", 253 | # Additional stuff for the LaTeX preamble. 254 | "preamble": "", 255 | # Latex figure (float) alignment 256 | "figure_align": "htbp", 257 | } 258 | 259 | # Grouping the document tree into LaTeX files. List of tuples 260 | # (source start file, target name, title, 261 | # author, documentclass [howto, manual, or own class]). 262 | latex_documents = [ 263 | ( 264 | master_doc, 265 | "CircuitPythonCirquePinnacleLibrary.tex", 266 | "CircuitPython Cirque Pinnacle Library Documentation", 267 | author, 268 | "manual", 269 | ), 270 | ] 271 | 272 | # -- Options for manual page output 273 | # --------------------------------------- 274 | 275 | # One entry per manual page. List of tuples 276 | # (source start file, name, description, authors, manual section). 277 | man_pages = [ 278 | ( 279 | master_doc, 280 | "CircuitPythonCirquePinnacleLibrary", 281 | "CircuitPython Cirque Pinnacle Library Documentation", 282 | [author], 283 | 1, 284 | ) 285 | ] 286 | 287 | # -- Options for Texinfo output 288 | # ------------------------------------------- 289 | 290 | # Grouping the document tree into Texinfo files. List of tuples 291 | # (source start file, target name, title, author, 292 | # dir menu entry, description, category) 293 | texinfo_documents = [ 294 | ( 295 | master_doc, 296 | "CircuitpythonCirquePinnacleLibrary", 297 | "Circuitpython Cirque Pinnacle Library Documentation", 298 | author, 299 | "CircuitpythonCirquePinnacleLibrary", 300 | "CircuitPython Library for Cirque Pinnacle touch Controller.", 301 | "Miscellaneous", 302 | ), 303 | ] 304 | 305 | # ---Options for PDF output 306 | # ----------------------------------------- 307 | # requires `rst2pdf` module which is not builtin to Python 3.4 nor 308 | # readthedocs.org's docker) 309 | 310 | pdf_documents = [ 311 | ( 312 | "index", 313 | "CircuitPython-Cirque-Pinnacle", 314 | "CircuitPython-Cirque-Pinnacle library documentation", 315 | "Brendan Doherty", 316 | ), 317 | ] 318 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | 2 | Examples 3 | ======== 4 | 5 | Relative Mode example 6 | --------------------- 7 | 8 | A simple example of using the Pinnacle ASIC in relative mode. 9 | 10 | .. literalinclude:: ../examples/cirque_pinnacle_relative_mode.py 11 | :caption: examples/cirque_pinnacle_relative_mode.py 12 | :linenos: 13 | :start-at: import time 14 | :end-before: def set_role() 15 | 16 | Absolute Mode example 17 | --------------------- 18 | 19 | A simple example of using the Pinnacle ASIC in absolute mode. 20 | 21 | .. literalinclude:: ../examples/cirque_pinnacle_absolute_mode.py 22 | :caption: examples/cirque_pinnacle_absolute_mode.py 23 | :linenos: 24 | :start-at: import time 25 | :end-before: def set_role() 26 | 27 | Anymeas mode example 28 | -------------------- 29 | 30 | This example uses the Pinnacle touch controller's anymeas mode to fetch raw ADC values. 31 | 32 | .. literalinclude:: ../examples/cirque_pinnacle_anymeas_mode.py 33 | :caption: examples/cirque_pinnacle_anymeas_mode.py 34 | :linenos: 35 | :start-at: import time 36 | :end-before: def set_role() 37 | :emphasize-lines: 30-35 38 | 39 | USB Mouse example 40 | ----------------- 41 | 42 | This example uses CircuitPython's built-in `usb_hid` API to emulate a mouse with the 43 | Cirque circle trackpad. 44 | 45 | .. literalinclude:: ../examples/cirque_pinnacle_usb_mouse.py 46 | :caption: examples/cirque_pinnacle_usb_mouse.py 47 | :linenos: 48 | :start-at: import time 49 | :end-before: def set_role() 50 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. toctree:: 3 | :hidden: 4 | 5 | self 6 | 7 | .. toctree:: 8 | :hidden: 9 | 10 | examples 11 | 12 | .. toctree:: 13 | :caption: API Reference 14 | :hidden: 15 | 16 | api 17 | rel_abs 18 | anymeas 19 | 20 | .. toctree:: 21 | :hidden: 22 | 23 | contributing 24 | 25 | .. toctree:: 26 | :caption: Related Products 27 | 28 | Cirque Glidepoint circle trackpads 29 | 12-pin FPC cable (0.5mm pitch) 30 | 31 | Introduction 32 | ============ 33 | 34 | A CircuitPython driver library that implements the Adafruit_BusDevice library 35 | for interfacing with the Cirque Pinnacle (1CA027) touch controller used in Cirque Circle Trackpads. 36 | 37 | Supported Features 38 | ------------------ 39 | 40 | * Use SPI or I2C bus protocols to interface with the Pinnacle touch controller ASIC (Application 41 | Specific Integrated Circuit). 42 | * Relative mode data reporting (AKA Mouse mode) with optional tap detection. 43 | * Absolute mode data reporting (x, y, & z axis positions). 44 | * AnyMeas mode data reporting. This mode exposes the ADC (Analog to Digital Converter) values and is 45 | not well documented in the numerous datasheets provided by the Cirque corporation about the 46 | Pinnacle (1CA027), thus this is a rather experimental mode. 47 | * Hardware input buttons' states included in data reports. There are 3 button input lines on 48 | the Cirque circle trackpads -- see `Pinout`_ section. 49 | * Configure measurements for finger or stylus (or automatically detirmine either) touch 50 | events. The Cirque circle trackpads are natively capable of measuring only 1 touch 51 | point per event. 52 | * Download/upload the underlying compensation matrix for ADC measurements. 53 | * Adjust the ADC matrix gain (sensitivity). 54 | 55 | .. tip:: 56 | The SPI protocol is the preferred method for interfacing with more than 1 Cirque circle 57 | trackpad from the same MCU (microcontroller). The Cirque Pinnacle does not allow 58 | changing the I2C slave device address (via software); this means only 1 Cirque circle trackpad 59 | can be accessed over the I2C bus in the lifecycle of an application. That said, you could change 60 | the I2C address from ``0x2A`` to ``0x2C`` by soldering a 470K ohm resistor at the junction 61 | labeled "ADR" (see picture in `Pinout`_ section), although this is untested. 62 | 63 | Unsupported Features 64 | -------------------- 65 | 66 | * The legacy PS\\2 interface is pretty limited and not accessible by some CircuitPython MCUs. 67 | Therefore, it has been neglected in this library. 68 | * Cirque's circle trackpads ship with the newer non-AG (Advanced Gestures) variant of the 69 | Pinnacle touch controller ASIC. Thus, this library focuses on the the non-AG variant's 70 | functionality via testing, and it does not provide access to the older AG variant's features 71 | (register addresses slightly differ which breaks compatibility). 72 | 73 | Pinout 74 | ------ 75 | 76 | .. warning:: 77 | The GPIO pins on these trackpads are **not** 5V tolerant. If your microcontroller uses 5V logic 78 | (ie Arduino Nano, Uno, Pro, Micro), then you must remove the resistors at junctions "R7" and "R8". 79 | Reportedly, this allows powering the trackpad with 5V (to VDD pin) and the trackpad GPIO pins become 80 | tolerant of 5V logic levels. 81 | .. image:: https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle/raw/master/docs/_static/Cirque_GlidePoint-Circle-Trackpad.png 82 | :target: https://www.mouser.com/new/cirque/glidepoint-circle-trackpads/ 83 | 84 | The above picture is an example of the Cirque GlidePoint circle trackpad. This picture 85 | is chosen as the test pads (larger copper circular pads) are clearly labeled. The test pads 86 | are extended to the `12-pin FFC/FPC cable 87 | `_ 88 | connector (the white block near the bottom). The following table shows how the pins are connected 89 | in the `examples `_ (tested on an `ItsyBitys M4 `_ and a Raspberry Pi 2) 90 | 91 | .. csv-table:: pinout (ordered the same as the FFC/FPC cable connector) 92 | :header: "cable pin number", Label, "MCU pin","RPi pin", Description 93 | :widths: 4, 5, 5, 5, 13 94 | 95 | 1, SCK, SCK, SCK, "SPI clock line" 96 | 2, SO, MISO, MISO, "Master Input Slave Output" 97 | 3, SS, D2, "CE0 (GPIO8)", "Slave Select (AKA Chip Select)" 98 | 4, DR, D7, GPIO25, "Data Ready interrupt" 99 | 5, SI, MOSI, MOSI, "SPI Master Output Slave Input" 100 | 6, B2, N/A, N/A, "Hardware input button #2" 101 | 7, B3, N/A, N/A, "Hardware input button #3" 102 | 8, B1, N/A, N/A, "Hardware input button #1" 103 | 9, SCL, SCL,, SCL "I2C clock line (no builtin pull-up resistor)" 104 | 10, SDA, SDA, SDA, "I2C data line (no builtin pull-up resistor)" 105 | 11, GND, GND, GND, Ground 106 | 12, VDD, 3V, 3V, "3V power supply" 107 | 108 | .. tip:: 109 | Of course, you can capture button data manually (if your application utilizes more 110 | than 3 buttons), but if you connect the pins B1, B2, B3 to momentary push buttons that 111 | (when pressed) provide a path to ground, the Pinnacle touch controller will report all 3 112 | buttons' states for each touch (or even button only) events. 113 | 114 | .. note:: 115 | These trackpads have no builtin pull-up resistors on the I2C bus' SDA and SCL lines. 116 | Examples were tested with a 4.7K ohm resistor for each I2C line tied to 3v. 117 | 118 | The Raspberry Pi boards (excluding any RP2040 boards) all have builtin 1.8K ohm pull-up 119 | resistors, so the Linux examples were tested with no addition pull-up resistance. 120 | 121 | .. _HCO: 122 | 123 | Model Labeling Scheme 124 | ********************* 125 | 126 | TM\ ``yyyxxx``\ -202\ ``i``\ -\ ``cc``\ ``o`` 127 | 128 | - ``yyyxxx`` stands for the respective vertical & horizontal width of the trackpad in millimeters. 129 | - ``i`` stands for the hardwired interface protocol (3 = I2C, 4 = SPI). Notice, if there is a 130 | resistor populated at the R1 (470K ohm) junction (located just above the Pinnacle ASIC), it 131 | is configured for SPI, otherwise it is configured for I2C. 132 | - ``cc`` stands for Custom Configuration which describes if a 470K ohm resistor is populated at 133 | junction R4. "30" (resistor at R4 exists) means that the hardware is configured to disable 134 | certain features despite what this library does. "00" (no resistor at R4) means that the 135 | hardware is configured to allow certain features to be manipulated by this library. These 136 | features include "secondary tap" (thought of as "right mouse button" in relative data mode), 137 | Intellimouse scrolling (Microsoft patented scroll wheel behavior -- a throw back to when 138 | scroll wheels were first introduced), and 180 degree orientation (your application can invert 139 | the axis data anyway). 140 | - ``o`` stands for the overlay type (0 = none, 1 = adhesive, 2 = flat, 3 = curved) 141 | 142 | Getting Started 143 | --------------- 144 | 145 | Dependencies 146 | ************ 147 | 148 | This driver depends on: 149 | 150 | * `Adafruit CircuitPython `_ 151 | * `Bus Device `_ 152 | 153 | Please ensure all dependencies are available on the CircuitPython filesystem. 154 | This is easily achieved by downloading `the Adafruit library and driver bundle 155 | `_. 156 | 157 | How to Install 158 | ************** 159 | 160 | Using ``pip`` 161 | ~~~~~~~~~~~~~ 162 | 163 | This library is deployed to pypi.org, so you can easily install this library 164 | using 165 | 166 | .. code-block:: shell 167 | 168 | pip3 install circuitpython-cirque-pinnacle 169 | 170 | Using git source 171 | ~~~~~~~~~~~~~~~~ 172 | 173 | This library can also be installed from the git source repository. 174 | 175 | .. code-block:: shell 176 | 177 | git clone https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle.git 178 | cd CircuitPython_Cirque_Pinnacle 179 | python3 -m pip install . 180 | 181 | Usage Example 182 | ************* 183 | 184 | Ensure you've connected the TMyyyxxx correctly by running the `examples/` located in the `examples 185 | folder of this library `_. 186 | 187 | Contributing 188 | ------------ 189 | 190 | Contributions are welcome! Please read our `Code of Conduct 191 | `_ 192 | before contributing to help this project stay welcoming. 193 | 194 | Please review our :doc:`contributing` for details on the development workflow and linting tools. 195 | 196 | To initiate a discussion of idea(s), you need only open an issue on the 197 | `source's git repository `_ 198 | (it doesn't have to be a bug report). 199 | 200 | Sphinx documentation 201 | ******************** 202 | 203 | Please read our :doc:`contributing` for instructions on how to build the documentation. 204 | -------------------------------------------------------------------------------- /docs/rel_abs.rst: -------------------------------------------------------------------------------- 1 | Relative or Absolute mode API 2 | ============================= 3 | 4 | Data Structures 5 | --------------- 6 | 7 | .. autoclass:: circuitpython_cirque_pinnacle.RelativeReport 8 | :members: 9 | 10 | .. autoclass:: circuitpython_cirque_pinnacle.AbsoluteReport 11 | :members: 12 | 13 | PinnacleTouch API 14 | ----------------- 15 | 16 | .. autoattribute:: circuitpython_cirque_pinnacle.PinnacleTouch.feed_enable 17 | 18 | .. autoattribute:: circuitpython_cirque_pinnacle.PinnacleTouch.hard_configured 19 | 20 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.relative_mode_config 21 | 22 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.absolute_mode_config 23 | 24 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.available 25 | 26 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.read 27 | 28 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.clear_status_flags 29 | 30 | .. autoattribute:: circuitpython_cirque_pinnacle.PinnacleTouch.allow_sleep 31 | 32 | .. autoattribute:: circuitpython_cirque_pinnacle.PinnacleTouch.shutdown 33 | 34 | .. autoattribute:: circuitpython_cirque_pinnacle.PinnacleTouch.sample_rate 35 | 36 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.detect_finger_stylus 37 | 38 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.calibrate 39 | 40 | .. autoattribute:: circuitpython_cirque_pinnacle.PinnacleTouch.calibration_matrix 41 | 42 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.set_adc_gain 43 | 44 | .. automethod:: circuitpython_cirque_pinnacle.PinnacleTouch.tune_edge_sensitivity 45 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-immaterial 2 | -------------------------------------------------------------------------------- /examples/cirque_pinnacle_absolute_mode.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple example of using the Pinnacle ASIC in absolute mode. 3 | """ 4 | 5 | import math 6 | import sys 7 | import time 8 | import board 9 | from digitalio import DigitalInOut 10 | from circuitpython_cirque_pinnacle import ( 11 | PinnacleTouchSPI, 12 | PinnacleTouchI2C, # noqa: imported-but-unused 13 | PINNACLE_ABSOLUTE, 14 | AbsoluteReport, 15 | ) 16 | 17 | IS_ON_LINUX = sys.platform.lower() == "linux" 18 | 19 | print("Cirque Pinnacle absolute mode\n") 20 | 21 | # the pin connected to the trackpad's DR pin. 22 | dr_pin = DigitalInOut(board.D7 if not IS_ON_LINUX else board.D25) 23 | 24 | if not input("Is the trackpad configured for I2C? [y/N] ").lower().startswith("y"): 25 | print("-- Using SPI interface.") 26 | spi = board.SPI() 27 | ss_pin = DigitalInOut(board.D2 if not IS_ON_LINUX else board.CE0) 28 | trackpad = PinnacleTouchSPI(spi, ss_pin, dr_pin) 29 | else: 30 | print("-- Using I2C interface.") 31 | i2c = board.I2C() 32 | trackpad = PinnacleTouchI2C(i2c, dr_pin) 33 | 34 | trackpad.data_mode = PINNACLE_ABSOLUTE # ensure Absolute mode is enabled 35 | trackpad.absolute_mode_config(z_idle_count=1) # limit idle packet count to 1 36 | 37 | # an object to hold the data reported by the Pinnacle 38 | data = AbsoluteReport() 39 | 40 | 41 | def print_data(timeout=6): 42 | """Print available data reports from the Pinnacle touch controller 43 | until there's no input for a period of ``timeout`` seconds.""" 44 | print( 45 | "Touch the trackpad to see the data. Exits after", 46 | timeout, 47 | "seconds of inactivity.", 48 | ) 49 | start = time.monotonic() 50 | while time.monotonic() - start < timeout: 51 | while trackpad.available(): # is there new data? 52 | trackpad.read(data) 53 | # specification sheet recommends clamping absolute position data of 54 | # X & Y axis for reliability 55 | if data.z: # only clamp values if Z axis is not idle. 56 | data.x = max(128, min(1920, data.x)) # X-axis 57 | data.y = max(64, min(1472, data.y)) # Y-axis 58 | print(data) 59 | start = time.monotonic() 60 | 61 | 62 | def print_trig(timeout=6): 63 | """Print available data reports from the Pinnacle touch controller as trigonometric 64 | calculations until there's no input for a period of ``timeout`` seconds.""" 65 | print( 66 | "Touch the trackpad to see the data. Exits after", 67 | timeout, 68 | "seconds of inactivity.", 69 | ) 70 | start = time.monotonic() 71 | while time.monotonic() - start < timeout: 72 | while trackpad.available(): # is there new data? 73 | trackpad.read(data) 74 | 75 | if not data.z: # if not touching (or near) the sensor 76 | print("Idling") # don't do calc when both axes are 0 77 | else: # if touching (or near) the sensor 78 | # datasheet recommends clamping X & Y axis for reliability 79 | data.x = max(128, min(1920, data.x)) # 128 <= x <= 1920 80 | data.y = max(64, min(1472, data.y)) # 64 <= y <= 1472 81 | 82 | # coordinates assume axes have been clamped to recommended ranges 83 | coord_x = data.x - 960 84 | coord_y = data.y - 736 # NOTE: y-axis is inverted by default 85 | radius = math.sqrt(math.pow(coord_x, 2) + math.pow(coord_y, 2)) 86 | # angle (in degrees) ranges [-180, 180]; 87 | angle = math.atan2(coord_y, coord_x) * 180 / math.pi 88 | print("angle: %.02f\tradius: %.02f" % (angle, radius)) 89 | start = time.monotonic() 90 | 91 | 92 | def set_role(): 93 | """Set the role using stdin stream. Arguments for functions can be 94 | specified using a space delimiter (e.g. 'M 10' calls `print_data(10)`) 95 | """ 96 | user_input = ( 97 | input( 98 | "\n*** Enter 'M' to measure and print raw data." 99 | "\n*** Enter 'T' to measure and print trigonometric calculations." 100 | "\n*** Enter 'Q' to quit example.\n" 101 | ) 102 | or "?" 103 | ).split() 104 | if user_input[0].upper().startswith("M"): 105 | print_data(*[int(x) for x in user_input[1:2]]) 106 | return True 107 | if user_input[0].upper().startswith("T"): 108 | print_trig(*[int(x) for x in user_input[1:2]]) 109 | return True 110 | if user_input[0].upper().startswith("Q"): 111 | return False 112 | print(user_input[0], "is an unrecognized input. Please try again.") 113 | return set_role() 114 | 115 | 116 | if __name__ == "__main__": 117 | try: 118 | while set_role(): 119 | pass # continue example until 'Q' is entered 120 | except KeyboardInterrupt: 121 | print(" Keyboard Interrupt detected.") 122 | else: 123 | print( 124 | "\nRun print_data() to read and print raw data.", 125 | "Run print_trig() to measure and print trigonometric calculations.", 126 | sep="\n", 127 | ) 128 | -------------------------------------------------------------------------------- /examples/cirque_pinnacle_anymeas_mode.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple example of using the Pinnacle ASIC in anymeas mode. 3 | """ 4 | 5 | import sys 6 | import time 7 | import board 8 | from digitalio import DigitalInOut 9 | from circuitpython_cirque_pinnacle import ( 10 | PinnacleTouchSPI, 11 | PinnacleTouchI2C, 12 | PINNACLE_ANYMEAS, 13 | ) 14 | 15 | IS_ON_LINUX = sys.platform.lower() == "linux" 16 | 17 | print("Cirque Pinnacle anymeas mode\n") 18 | 19 | # the pin connected to the trackpad's DR pin. 20 | dr_pin = DigitalInOut(board.D7 if not IS_ON_LINUX else board.D25) 21 | 22 | if not input("Is the trackpad configured for I2C? [y/N] ").lower().startswith("y"): 23 | print("-- Using SPI interface.") 24 | spi = board.SPI() 25 | ss_pin = DigitalInOut(board.D2 if not IS_ON_LINUX else board.CE0) 26 | trackpad = PinnacleTouchSPI(spi, ss_pin, dr_pin) 27 | else: 28 | print("-- Using I2C interface.") 29 | i2c = board.I2C() 30 | trackpad = PinnacleTouchI2C(i2c, dr_pin) 31 | 32 | trackpad.data_mode = PINNACLE_ANYMEAS 33 | 34 | vectors = [ 35 | # toggle , polarity 36 | (0x00010000, 0x00010000), # This toggles Y0 only and toggles it positively 37 | (0x00010000, 0x00000000), # This toggles Y0 only and toggles it negatively 38 | (0x00000001, 0x00000000), # This toggles X0 only and toggles it positively 39 | (0x00008000, 0x00000000), # This toggles X16 only and toggles it positively 40 | (0x00FF00FF, 0x000000FF), # This toggles Y0-Y7 negative and X0-X7 positive 41 | ] 42 | 43 | # a list of compensations to use with measured `vectors` 44 | compensation = [0] * len(vectors) 45 | 46 | 47 | def compensate(count=5): 48 | """Take ``count`` measurements, then average them together (for each vector)""" 49 | for i, (toggle, polarity) in enumerate(vectors): 50 | compensation[i] = 0 51 | for _ in range(count): 52 | result = trackpad.measure_adc(toggle, polarity) 53 | compensation[i] += result 54 | compensation[i] = int(compensation[i] / count) 55 | print("compensation {}: {}".format(i, compensation[i])) 56 | 57 | 58 | def take_measurements(timeout=6): 59 | """Read ``len(vectors)`` number of measurements and print results for 60 | ``timeout`` number of seconds.""" 61 | print("Taking measurements for", timeout, "seconds.") 62 | start = time.monotonic() 63 | while time.monotonic() - start < timeout: 64 | for i, (toggle, polarity) in enumerate(vectors): 65 | result = trackpad.measure_adc(toggle, polarity) 66 | print("meas{}: {}".format(i, result - compensation[i]), end="\t") 67 | print() 68 | 69 | 70 | def set_role(): 71 | """Set the role using stdin stream. Arguments for functions can be 72 | specified using a space delimiter (e.g. 'C 10' calls `compensate(10)`) 73 | """ 74 | user_input = ( 75 | input( 76 | "\n*** Enter 'C' to get compensations for measurements." 77 | "\n*** Enter 'M' to read and print measurements." 78 | "\n*** Enter 'Q' to quit example.\n" 79 | ) 80 | or "?" 81 | ).split() 82 | if user_input[0].upper().startswith("C"): 83 | compensate(*[int(x) for x in user_input[1:2]]) 84 | return True 85 | if user_input[0].upper().startswith("M"): 86 | take_measurements(*[int(x) for x in user_input[1:2]]) 87 | return True 88 | if user_input[0].upper().startswith("Q"): 89 | return False 90 | print(user_input[0], "is an unrecognized input. Please try again.") 91 | return set_role() 92 | 93 | 94 | if __name__ == "__main__": 95 | try: 96 | while set_role(): 97 | pass # continue example until 'Q' is entered 98 | except KeyboardInterrupt: 99 | print(" Keyboard Interrupt detected.") 100 | else: 101 | print( 102 | "\nRun compensate() to set compensations for measurements.", 103 | "Run take_measurements() to read and print measurements.", 104 | sep="\n", 105 | ) 106 | -------------------------------------------------------------------------------- /examples/cirque_pinnacle_relative_mode.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple example of using the Pinnacle ASIC in relative mode. 3 | """ 4 | 5 | import sys 6 | import time 7 | import board 8 | from digitalio import DigitalInOut 9 | from circuitpython_cirque_pinnacle import ( 10 | PinnacleTouchSPI, 11 | PinnacleTouchI2C, 12 | RelativeReport, 13 | PINNACLE_RELATIVE, 14 | ) 15 | 16 | IS_ON_LINUX = sys.platform.lower() == "linux" 17 | 18 | print("Cirque Pinnacle relative mode\n") 19 | 20 | # the pin connected to the trackpad's DR pin. 21 | dr_pin = DigitalInOut(board.D7 if not IS_ON_LINUX else board.D25) 22 | 23 | if not input("Is the trackpad configured for I2C? [y/N] ").lower().startswith("y"): 24 | print("-- Using SPI interface.") 25 | spi = board.SPI() 26 | ss_pin = DigitalInOut(board.D2 if not IS_ON_LINUX else board.CE0) 27 | trackpad = PinnacleTouchSPI(spi, ss_pin, dr_pin) 28 | else: 29 | print("-- Using I2C interface.") 30 | i2c = board.I2C() 31 | trackpad = PinnacleTouchI2C(i2c, dr_pin) 32 | 33 | trackpad.data_mode = PINNACLE_RELATIVE # ensure mouse mode is enabled 34 | trackpad.relative_mode_config(True) # enable tap detection 35 | 36 | # an object to hold the data reported by the Pinnacle 37 | data = RelativeReport() 38 | 39 | 40 | def print_data(timeout=6): 41 | """Print available data reports from the Pinnacle touch controller 42 | until there's no input for a period of ``timeout`` seconds.""" 43 | print( 44 | "Touch the trackpad to see the data. Exits after", 45 | timeout, 46 | "seconds of inactivity.", 47 | ) 48 | start = time.monotonic() 49 | while time.monotonic() - start < timeout: 50 | while trackpad.available(): # is there new data? 51 | trackpad.read(data) 52 | print(data) 53 | start = time.monotonic() 54 | 55 | 56 | def set_role(): 57 | """Set the role using stdin stream. Arguments for functions can be 58 | specified using a space delimiter (e.g. 'M 10' calls `print_data(10)`) 59 | """ 60 | user_input = ( 61 | input( 62 | "\n*** Enter 'M' to measure and print data." 63 | "\n*** Enter 'Q' to quit example.\n" 64 | ) 65 | or "?" 66 | ).split() 67 | if user_input[0].upper().startswith("M"): 68 | print_data(*[int(x) for x in user_input[1:2]]) 69 | return True 70 | if user_input[0].upper().startswith("Q"): 71 | return False 72 | print(user_input[0], "is an unrecognized input. Please try again.") 73 | return set_role() 74 | 75 | 76 | if __name__ == "__main__": 77 | try: 78 | while set_role(): 79 | pass # continue example until 'Q' is entered 80 | except KeyboardInterrupt: 81 | print(" Keyboard Interrupt detected.") 82 | else: 83 | print("\nRun print_data() to measure and print data.") 84 | -------------------------------------------------------------------------------- /examples/cirque_pinnacle_usb_mouse.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example uses CircuitPython's built-in `usb_hid` API 3 | to emulate a mouse with the Cirque circle trackpad. 4 | 5 | NOTE: This example won't work on Linux (eg. using Raspberry Pi GPIO pins). 6 | """ 7 | 8 | import sys 9 | import time 10 | import board 11 | from digitalio import DigitalInOut 12 | import usb_hid 13 | from circuitpython_cirque_pinnacle import ( 14 | PinnacleTouchSPI, 15 | PinnacleTouchI2C, 16 | PINNACLE_RELATIVE, 17 | RelativeReport, 18 | ) 19 | 20 | IS_ON_LINUX = sys.platform.lower() == "linux" 21 | 22 | print("Cirque Pinnacle as a USB mouse\n") 23 | 24 | # the pin connected to the trackpad's DR pin. 25 | dr_pin = DigitalInOut(board.D7 if not IS_ON_LINUX else board.D25) 26 | 27 | if not input("Is the trackpad configured for I2C? [y/N] ").lower().startswith("y"): 28 | print("-- Using SPI interface.") 29 | spi = board.SPI() 30 | ss_pin = DigitalInOut(board.D2 if not IS_ON_LINUX else board.CE0) 31 | trackpad = PinnacleTouchSPI(spi, ss_pin, dr_pin) 32 | else: 33 | print("-- Using I2C interface.") 34 | i2c = board.I2C() 35 | trackpad = PinnacleTouchI2C(i2c, dr_pin) 36 | 37 | trackpad.data_mode = PINNACLE_RELATIVE # ensure mouse mode is enabled 38 | # tell the Pinnacle ASIC to rotate the orientation of the axis data by +90 degrees 39 | trackpad.relative_mode_config(rotate90=True) 40 | 41 | # an object to hold the data reported by the Pinnacle 42 | data = RelativeReport() 43 | 44 | mouse = None 45 | for dev in usb_hid.devices: 46 | # be sure we're grabbing the mouse singleton 47 | if dev.usage == 2 and dev.usage_page == 1: 48 | mouse = dev 49 | break 50 | else: 51 | raise OSError("mouse HID device not available.") 52 | # mouse.send_report() takes a 4 byte buffer in which 53 | # byte0 = buttons in which 54 | # bit5 = back, bit4 = forward, bit2 = middle, bit1 = right, bit0 = left 55 | # byte1 = delta x-axis 56 | # byte2 = delta y-axis 57 | # byte3 = delta scroll wheel 58 | 59 | 60 | def move(timeout=10): 61 | """Send mouse X & Y reported data from the Pinnacle touch controller 62 | until there's no input for a period of ``timeout`` seconds.""" 63 | print( 64 | "Trackpad acting as a USB mouse device until", timeout, "seconds of inactivity." 65 | ) 66 | start = time.monotonic() 67 | while time.monotonic() - start < timeout: 68 | while trackpad.available(): 69 | trackpad.read(data) 70 | data.x *= -1 # invert x-axis 71 | mouse.send_report(data.buffer) 72 | start = time.monotonic() # reset timeout 73 | 74 | mouse.send_report(b"\x00" * 4) # release buttons (just in case) 75 | 76 | 77 | def set_role(): 78 | """Set the role using stdin stream. Arguments for functions can be 79 | specified using a space delimiter (e.g. 'M 10' calls `move(10)`) 80 | """ 81 | user_input = ( 82 | input( 83 | "\n*** Enter 'M' to control the mouse with the trackpad." 84 | "\n*** Enter 'Q' to quit example.\n" 85 | ) 86 | or "?" 87 | ).split() 88 | if user_input[0].upper().startswith("M"): 89 | move(*[int(x) for x in user_input[1:2]]) 90 | return True 91 | if user_input[0].upper().startswith("Q"): 92 | return False 93 | print(user_input[0], "is an unrecognized input. Please try again.") 94 | return set_role() 95 | 96 | 97 | if __name__ == "__main__": 98 | try: 99 | while set_role(): 100 | pass # continue example until 'Q' is entered 101 | except KeyboardInterrupt: 102 | print(" Keyboard Interrupt detected.") 103 | else: 104 | print("\nRun move() to control the mouse with the trackpad.") 105 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [build-system] 3 | requires = [ 4 | "setuptools>=61", 5 | "wheel", 6 | "setuptools-scm", 7 | ] 8 | 9 | [project] 10 | name = "circuitpython-cirque-pinnacle" 11 | requires-python = ">=3.7" 12 | description = "A CircuitPython driver for Cirque Pinnacle (1CA027) touch controller used in Cirque Trackpads" 13 | readme = "README.rst" 14 | authors = [ 15 | {name = "Brendan Doherty", email = "2bndy5@gmail.com"} 16 | ] 17 | keywords = [ 18 | "blinka", 19 | "circuitpython", 20 | "raspberrypi", 21 | "driver", 22 | "Cirque", 23 | "Pinnacle", 24 | "touch", 25 | "sensor", 26 | "trackpad", 27 | ] 28 | license = {text = "MIT"} 29 | classifiers = [ 30 | "Development Status :: 3 - Alpha", 31 | "Intended Audience :: Developers", 32 | "Topic :: Software Development :: Libraries", 33 | "Topic :: System :: Hardware", 34 | "License :: OSI Approved :: MIT License", 35 | "Programming Language :: Python :: 3", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10", 40 | ] 41 | dynamic = ["version", "dependencies"] 42 | 43 | [project.urls] 44 | Documentation = "https://circuitpython-cirque-pinnacle.readthedocs.io" 45 | Source = "https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle" 46 | Tracker = "https://github.com/2bndy5/CircuitPython_Cirque_Pinnacle/issues" 47 | 48 | [tool.setuptools] 49 | py-modules = ["circuitpython_cirque_pinnacle"] 50 | 51 | [tool.setuptools.dynamic] 52 | dependencies = {file = ["requirements.txt"]} 53 | 54 | [tool.setuptools_scm] 55 | # It would be nice to include the commit hash in the version, but that 56 | # can't be done in a PEP 440-compatible way. 57 | version_scheme= "no-guess-dev" 58 | # Test PyPI does not support local versions. 59 | local_scheme = "no-local-version" 60 | fallback_version = "0.0.0" 61 | 62 | [tool.pytest.ini_options] 63 | minversion = "6.0" 64 | addopts = "-vv" 65 | testpaths = ["tests"] 66 | log_level = "DEBUG" 67 | log_format = "%(levelname)s\t%(name)s: %(message)s" 68 | 69 | [tool.mypy] 70 | show_error_codes = true 71 | pretty = true 72 | files = ["circuitpython_cirque_pinnacle.py"] 73 | exclude = "setup.py" 74 | check_untyped_defs = true 75 | 76 | [tool.pylint.main] 77 | # Analyse import fallback blocks. This can be used to support both Python 2 and 3 78 | # compatible code, which means that the block might have code that exists only in 79 | # one or another interpreter, leading to false positives when analysed. 80 | # analyse-fallback-blocks = 81 | 82 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint in 83 | # a server-like mode. 84 | # clear-cache-post-run = 85 | 86 | # Always return a 0 (non-error) status code, even if lint errors are found. This 87 | # is primarily useful in continuous integration scripts. 88 | # exit-zero = 89 | 90 | # A comma-separated list of package or module names from where C extensions may 91 | # be loaded. Extensions are loading into the active Python interpreter and may 92 | # run arbitrary code. 93 | # extension-pkg-allow-list = 94 | 95 | # A comma-separated list of package or module names from where C extensions may 96 | # be loaded. Extensions are loading into the active Python interpreter and may 97 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 98 | # for backward compatibility.) 99 | # extension-pkg-whitelist = 100 | 101 | # Return non-zero exit code if any of these messages/categories are detected, 102 | # even if score is above --fail-under value. Syntax same as enable. Messages 103 | # specified are enabled, while categories only check already-enabled messages. 104 | # fail-on = 105 | 106 | # Specify a score threshold under which the program will exit with error. 107 | fail-under = 10 108 | 109 | # Interpret the stdin as a python script, whose filename needs to be passed as 110 | # the module_or_package argument. 111 | # from-stdin = 112 | 113 | # Files or directories to be skipped. They should be base names, not paths. 114 | ignore = ["CVS"] 115 | 116 | # Add files or directories matching the regular expressions patterns to the 117 | # ignore-list. The regex matches against paths and can be in Posix or Windows 118 | # format. Because '\\' represents the directory delimiter on Windows systems, it 119 | # can't be used as an escape character. 120 | # ignore-paths = 121 | 122 | # Files or directories matching the regular expression patterns are skipped. The 123 | # regex matches against base names, not paths. The default value ignores Emacs 124 | # file locks 125 | # ignore-patterns = 126 | 127 | # List of module names for which member attributes should not be checked (useful 128 | # for modules/projects where namespaces are manipulated during runtime and thus 129 | # existing member attributes cannot be deduced by static analysis). It supports 130 | # qualified module names, as well as Unix pattern matching. 131 | ignored-modules = ["board"] 132 | 133 | # Python code to execute, usually for sys.path manipulation such as 134 | # pygtk.require(). 135 | # init-hook = 136 | 137 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 138 | # number of processors available to use, and will cap the count on Windows to 139 | # avoid hangs. 140 | jobs = 2 141 | 142 | # Control the amount of potential inferred values when inferring a single object. 143 | # This can help the performance when dealing with large functions or complex, 144 | # nested conditions. 145 | limit-inference-results = 100 146 | 147 | # List of plugins (as comma separated values of python module names) to load, 148 | # usually to register additional checkers. 149 | # load-plugins = 150 | 151 | # Pickle collected data for later comparisons. 152 | persistent = true 153 | 154 | # Minimum Python version to use for version dependent checks. Will default to the 155 | # version used to run pylint. 156 | py-version = "3.11" 157 | 158 | # Discover python modules and packages in the file system subtree. 159 | # recursive = 160 | 161 | # When enabled, pylint would attempt to guess common misconfiguration and emit 162 | # user-friendly hints instead of false-positive error messages. 163 | suggestion-mode = true 164 | 165 | # Allow loading of arbitrary C extensions. Extensions are imported into the 166 | # active Python interpreter and may run arbitrary code. 167 | # unsafe-load-any-extension = 168 | 169 | [tool.pylint.basic] 170 | # Naming style matching correct argument names. 171 | argument-naming-style = "snake_case" 172 | 173 | # Regular expression matching correct argument names. Overrides argument-naming- 174 | # style. If left empty, argument names will be checked with the set naming style. 175 | argument-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" 176 | 177 | # Naming style matching correct attribute names. 178 | attr-naming-style = "snake_case" 179 | 180 | # Regular expression matching correct attribute names. Overrides attr-naming- 181 | # style. If left empty, attribute names will be checked with the set naming 182 | # style. 183 | attr-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" 184 | 185 | # Bad variable names which should always be refused, separated by a comma. 186 | bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] 187 | 188 | # Bad variable names regexes, separated by a comma. If names match any regex, 189 | # they will always be refused 190 | # bad-names-rgxs = 191 | 192 | # Naming style matching correct class attribute names. 193 | class-attribute-naming-style = "any" 194 | 195 | # Regular expression matching correct class attribute names. Overrides class- 196 | # attribute-naming-style. If left empty, class attribute names will be checked 197 | # with the set naming style. 198 | class-attribute-rgx = "([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$" 199 | 200 | # Naming style matching correct class constant names. 201 | class-const-naming-style = "UPPER_CASE" 202 | 203 | # Regular expression matching correct class constant names. Overrides class- 204 | # const-naming-style. If left empty, class constant names will be checked with 205 | # the set naming style. 206 | # class-const-rgx = 207 | 208 | # Naming style matching correct class names. 209 | class-naming-style = "PascalCase" 210 | 211 | # Regular expression matching correct class names. Overrides class-naming-style. 212 | # If left empty, class names will be checked with the set naming style. 213 | class-rgx = "[A-Z_][a-zA-Z0-9_]+$" 214 | 215 | # Naming style matching correct constant names. 216 | const-naming-style = "UPPER_CASE" 217 | 218 | # Regular expression matching correct constant names. Overrides const-naming- 219 | # style. If left empty, constant names will be checked with the set naming style. 220 | const-rgx = "(([A-Z_][A-Z0-9_]*)|(__.*__))$" 221 | 222 | # Minimum line length for functions/classes that require docstrings, shorter ones 223 | # are exempt. 224 | docstring-min-length = -1 225 | 226 | # Naming style matching correct function names. 227 | function-naming-style = "snake_case" 228 | 229 | # Regular expression matching correct function names. Overrides function-naming- 230 | # style. If left empty, function names will be checked with the set naming style. 231 | function-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" 232 | 233 | # Good variable names which should always be accepted, separated by a comma. 234 | good-names = ["r", "g", "b", "w", "i", "j", "k", "n", "x", "y", "z", "ex", "ok", "Run", "_"] 235 | 236 | # Good variable names regexes, separated by a comma. If names match any regex, 237 | # they will always be accepted 238 | # good-names-rgxs = 239 | 240 | # Include a hint for the correct naming format with invalid-name. 241 | # include-naming-hint = 242 | 243 | # Naming style matching correct inline iteration names. 244 | inlinevar-naming-style = "any" 245 | 246 | # Regular expression matching correct inline iteration names. Overrides 247 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 248 | # with the set naming style. 249 | inlinevar-rgx = "[A-Za-z_][A-Za-z0-9_]*$" 250 | 251 | # Naming style matching correct method names. 252 | method-naming-style = "snake_case" 253 | 254 | # Regular expression matching correct method names. Overrides method-naming- 255 | # style. If left empty, method names will be checked with the set naming style. 256 | method-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" 257 | 258 | # Naming style matching correct module names. 259 | module-naming-style = "snake_case" 260 | 261 | # Regular expression matching correct module names. Overrides module-naming- 262 | # style. If left empty, module names will be checked with the set naming style. 263 | module-rgx = "(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$" 264 | 265 | # Colon-delimited sets of names that determine each other's naming style when the 266 | # name regexes allow several styles. 267 | # name-group = 268 | 269 | # Regular expression which should only match function or class names that do not 270 | # require a docstring. 271 | no-docstring-rgx = "^_" 272 | 273 | # List of decorators that produce properties, such as abc.abstractproperty. Add 274 | # to this list to register other decorators that produce valid properties. These 275 | # decorators are taken in consideration only for invalid-name. 276 | property-classes = ["abc.abstractproperty"] 277 | 278 | # Regular expression matching correct type variable names. If left empty, type 279 | # variable names will be checked with the set naming style. 280 | # typevar-rgx = 281 | 282 | # Naming style matching correct variable names. 283 | variable-naming-style = "snake_case" 284 | 285 | # Regular expression matching correct variable names. Overrides variable-naming- 286 | # style. If left empty, variable names will be checked with the set naming style. 287 | variable-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" 288 | 289 | [tool.pylint.classes] 290 | # Warn about protected attribute access inside special methods 291 | # check-protected-access-in-special-methods = 292 | 293 | # List of method names used to declare (i.e. assign) instance attributes. 294 | defining-attr-methods = ["__init__", "__new__", "setUp"] 295 | 296 | # List of member names, which should be excluded from the protected access 297 | # warning. 298 | exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make"] 299 | 300 | # List of valid names for the first argument in a class method. 301 | valid-classmethod-first-arg = ["cls"] 302 | 303 | # List of valid names for the first argument in a metaclass class method. 304 | valid-metaclass-classmethod-first-arg = ["mcs"] 305 | 306 | [tool.pylint.design] 307 | # List of regular expressions of class ancestor names to ignore when counting 308 | # public methods (see R0903) 309 | # exclude-too-few-public-methods = 310 | 311 | # List of qualified class names to ignore when counting class parents (see R0901) 312 | # ignored-parents = 313 | 314 | # Maximum number of arguments for function / method. 315 | max-args = 5 316 | 317 | # Maximum number of attributes for a class (see R0902). 318 | max-attributes = 11 319 | 320 | # Maximum number of boolean expressions in an if statement (see R0916). 321 | max-bool-expr = 5 322 | 323 | # Maximum number of branch for function / method body. 324 | max-branches = 12 325 | 326 | # Maximum number of locals for function / method body. 327 | max-locals = 15 328 | 329 | # Maximum number of parents for a class (see R0901). 330 | max-parents = 7 331 | 332 | # Maximum number of public methods for a class (see R0904). 333 | max-public-methods = 20 334 | 335 | # Maximum number of return / yield for function / method body. 336 | max-returns = 6 337 | 338 | # Maximum number of statements in function / method body. 339 | max-statements = 50 340 | 341 | # Minimum number of public methods for a class (see R0903). 342 | min-public-methods = 1 343 | 344 | [tool.pylint.exceptions] 345 | # Exceptions that will emit a warning when caught. 346 | overgeneral-exceptions = ["builtins.Exception"] 347 | 348 | [tool.pylint.format] 349 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 350 | expected-line-ending-format = "LF" 351 | 352 | # Regexp for a line that is allowed to be longer than the limit. 353 | ignore-long-lines = "^\\s*(# )??$" 354 | 355 | # Number of spaces of indent required inside a hanging or continued line. 356 | indent-after-paren = 4 357 | 358 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 359 | # tab). 360 | indent-string = " " 361 | 362 | # Maximum number of characters on a single line. 363 | max-line-length = 100 364 | 365 | # Maximum number of lines in a module. 366 | max-module-lines = 1000 367 | 368 | # Allow the body of a class to be on the same line as the declaration if body 369 | # contains single statement. 370 | # single-line-class-stmt = 371 | 372 | # Allow the body of an if to be on the same line as the test if there is no else. 373 | # single-line-if-stmt = 374 | 375 | [tool.pylint.imports] 376 | # List of modules that can be imported at any level, not just the top level one. 377 | # allow-any-import-level = 378 | 379 | # Allow explicit reexports by alias from a package __init__. 380 | # allow-reexport-from-package = 381 | 382 | # Allow wildcard imports from modules that define __all__. 383 | # allow-wildcard-with-all = 384 | 385 | # Deprecated modules which should not be used, separated by a comma. 386 | deprecated-modules = ["optparse", "tkinter.tix"] 387 | 388 | # Output a graph (.gv or any supported image format) of external dependencies to 389 | # the given file (report RP0402 must not be disabled). 390 | # ext-import-graph = 391 | 392 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 393 | # external) dependencies to the given file (report RP0402 must not be disabled). 394 | # import-graph = 395 | 396 | # Output a graph (.gv or any supported image format) of internal dependencies to 397 | # the given file (report RP0402 must not be disabled). 398 | # int-import-graph = 399 | 400 | # Force import order to recognize a module as part of the standard compatibility 401 | # libraries. 402 | # known-standard-library = 403 | 404 | # Force import order to recognize a module as part of a third party library. 405 | known-third-party = ["enchant"] 406 | 407 | # Couples of modules and preferred modules, separated by a comma. 408 | # preferred-modules = 409 | 410 | [tool.pylint.logging] 411 | # The type of string formatting that logging methods do. `old` means using % 412 | # formatting, `new` is for `{}` formatting. 413 | logging-format-style = "old" 414 | 415 | # Logging modules to check that the string format arguments are in logging 416 | # function parameter format. 417 | logging-modules = ["logging"] 418 | 419 | [tool.pylint."messages control"] 420 | # Only show warnings with the listed confidence levels. Leave empty to show all. 421 | # Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 422 | confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] 423 | 424 | # Disable the message, report, category or checker with the given id(s). You can 425 | # either give multiple identifiers separated by comma (,) or put this option 426 | # multiple times (only on the command line, not in the configuration file where 427 | # it should appear only once). You can also use "--disable=all" to disable 428 | # everything first and then re-enable specific checks. For example, if you want 429 | # to run only the similarities checker, you can use "--disable=all 430 | # --enable=similarities". If you want to run only the classes checker, but have 431 | # no Warning level messages displayed, use "--disable=all --enable=classes 432 | # --disable=W". 433 | disable = [ 434 | "import-error", 435 | "duplicate-code", 436 | "consider-using-f-string", 437 | "too-many-arguments" 438 | ] 439 | 440 | # Enable the message, report, category or checker with the given id(s). You can 441 | # either give multiple identifier separated by comma (,) or put this option 442 | # multiple time (only on the command line, not in the configuration file where it 443 | # should appear only once). See also the "--disable" option for examples. 444 | enable = ["c-extension-no-member"] 445 | 446 | [tool.pylint.method_args] 447 | # List of qualified names (i.e., library.method) which require a timeout 448 | # parameter e.g. 'requests.api.get,requests.api.post' 449 | timeout-methods = ["requests.api.delete", "requests.api.get", "requests.api.head", "requests.api.options", "requests.api.patch", "requests.api.post", "requests.api.put", "requests.api.request"] 450 | 451 | [tool.pylint.miscellaneous] 452 | # List of note tags to take in consideration, separated by a comma. 453 | notes = ["FIXME", "XXX"] 454 | 455 | # Regular expression of note tags to take in consideration. 456 | # notes-rgx = 457 | 458 | [tool.pylint.refactoring] 459 | # Maximum number of nested blocks for function / method body 460 | max-nested-blocks = 5 461 | 462 | # Complete name of functions that never returns. When checking for inconsistent- 463 | # return-statements if a never returning function is called then it will be 464 | # considered as an explicit return statement and no message will be printed. 465 | never-returning-functions = ["sys.exit", "argparse.parse_error"] 466 | 467 | [tool.pylint.reports] 468 | # Python expression which should return a score less than or equal to 10. You 469 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 470 | # 'convention', and 'info' which contain the number of messages in each category, 471 | # as well as 'statement' which is the total number of statements analyzed. This 472 | # score is used by the global evaluation report (RP0004). 473 | evaluation = "10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)" 474 | 475 | # Template used to display messages. This is a python new-style format string 476 | # used to format the message information. See doc for all details. 477 | # msg-template = 478 | 479 | # Set the output format. Available formats are text, parseable, colorized, json 480 | # and msvs (visual studio). You can also give a reporter class, e.g. 481 | # mypackage.mymodule.MyReporterClass. 482 | # output-format = 483 | 484 | # Tells whether to display a full report or only the messages. 485 | # reports = 486 | 487 | # Activate the evaluation score. 488 | score = true 489 | 490 | [tool.pylint.similarities] 491 | # Comments are removed from the similarity computation 492 | ignore-comments = true 493 | 494 | # Docstrings are removed from the similarity computation 495 | ignore-docstrings = true 496 | 497 | # Imports are removed from the similarity computation 498 | # ignore-imports = 499 | 500 | # Signatures are removed from the similarity computation 501 | ignore-signatures = true 502 | 503 | # Minimum lines number of a similarity. 504 | min-similarity-lines = 4 505 | 506 | [tool.pylint.spelling] 507 | # Limits count of emitted suggestions for spelling mistakes. 508 | max-spelling-suggestions = 4 509 | 510 | # Spelling dictionary name. Available dictionaries: none. To make it work, 511 | # install the 'python-enchant' package. 512 | # spelling-dict = 513 | 514 | # List of comma separated words that should be considered directives if they 515 | # appear at the beginning of a comment and should not be checked. 516 | spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:" 517 | 518 | # List of comma separated words that should not be checked. 519 | # spelling-ignore-words = 520 | 521 | # A path to a file that contains the private dictionary; one word per line. 522 | # spelling-private-dict-file = 523 | 524 | # Tells whether to store unknown words to the private dictionary (see the 525 | # --spelling-private-dict-file option) instead of raising a message. 526 | # spelling-store-unknown-words = 527 | 528 | [tool.pylint.typecheck] 529 | # List of decorators that produce context managers, such as 530 | # contextlib.contextmanager. Add to this list to register other decorators that 531 | # produce valid context managers. 532 | contextmanager-decorators = ["contextlib.contextmanager"] 533 | 534 | # List of members which are set dynamically and missed by pylint inference 535 | # system, and so shouldn't trigger E1101 when accessed. Python regular 536 | # expressions are accepted. 537 | # generated-members = 538 | 539 | # Tells whether missing members accessed in mixin class should be ignored. A 540 | # class is considered mixin if its name matches the mixin-class-rgx option. 541 | # Tells whether to warn about missing members when the owner of the attribute is 542 | # inferred to be None. 543 | ignore-none = true 544 | 545 | # This flag controls whether pylint should warn about no-member and similar 546 | # checks whenever an opaque object is returned when inferring. The inference can 547 | # return multiple potential results while evaluating a Python object, but some 548 | # branches might not be evaluated, which results in partial inference. In that 549 | # case, it might be useful to still emit no-member and other checks for the rest 550 | # of the inferred objects. 551 | ignore-on-opaque-inference = true 552 | 553 | # List of symbolic message names to ignore for Mixin members. 554 | ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"] 555 | 556 | # List of class names for which member attributes should not be checked (useful 557 | # for classes with dynamically set attributes). This supports the use of 558 | # qualified names. 559 | ignored-classes = ["optparse.Values", "thread._local", "_thread._local"] 560 | 561 | # Show a hint with possible names when a member name was not found. The aspect of 562 | # finding the hint is based on edit distance. 563 | missing-member-hint = true 564 | 565 | # The minimum edit distance a name should have in order to be considered a 566 | # similar match for a missing member name. 567 | missing-member-hint-distance = 1 568 | 569 | # The total number of similar names that should be taken in consideration when 570 | # showing a hint for a missing member. 571 | missing-member-max-choices = 1 572 | 573 | # Regex pattern to define which classes are considered mixins. 574 | mixin-class-rgx = ".*[Mm]ixin" 575 | 576 | # List of decorators that change the signature of a decorated function. 577 | # signature-mutators = 578 | 579 | [tool.pylint.variables] 580 | # List of additional names supposed to be defined in builtins. Remember that you 581 | # should avoid defining new builtins when possible. 582 | # additional-builtins = 583 | 584 | # Tells whether unused global variables should be treated as a violation. 585 | allow-global-unused-variables = true 586 | 587 | # List of names allowed to shadow builtins 588 | # allowed-redefined-builtins = 589 | 590 | # List of strings which can identify a callback function by name. A callback name 591 | # must start or end with one of those strings. 592 | callbacks = ["cb_", "_cb"] 593 | 594 | # A regular expression matching the name of dummy variables (i.e. expected to not 595 | # be used). 596 | dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" 597 | 598 | # Argument names that match this expression will be ignored. 599 | ignored-argument-names = "_.*|^ignored_|^unused_" 600 | 601 | # Tells whether we should check for unused import in __init__ files. 602 | # init-import = 603 | 604 | # List of qualified module names which can have objects that can redefine 605 | # builtins. 606 | redefining-builtins-modules = ["six.moves", "future.builtins"] 607 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Adafruit-Blinka 2 | adafruit-circuitpython-busdevice 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """All setup/install info is now in pyproject.toml""" 2 | 3 | from setuptools import setup 4 | 5 | setup() 6 | --------------------------------------------------------------------------------