diff --git a/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md b/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md new file mode 100644 index 0000000..8de294e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2021 Adafruit Industries +# +# SPDX-License-Identifier: MIT + +Thank you for contributing! Before you submit a pull request, please read the following. + +Make sure any changes you're submitting are in line with the CircuitPython Design Guide, available here: https://docs.circuitpython.org/en/latest/docs/design_guide.html + +If your changes are to documentation, please verify that the documentation builds locally by following the steps found here: https://adafru.it/build-docs + +Before submitting the pull request, make sure you've run Pylint and Black locally on your code. You can do this manually or using pre-commit. Instructions are available here: https://adafru.it/check-your-code + +Please remove all of this text before submitting. Include an explanation or list of changes included in your PR, as well as, if applicable, a link to any related issues. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59baa53..041a337 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,68 +10,5 @@ jobs: test: runs-on: ubuntu-latest steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - name: Translate Repo Name For Build Tools filename_prefix - id: repo-name - run: | - echo ::set-output name=repo-name::$( - echo ${{ github.repository }} | - awk -F '\/' '{ print tolower($2) }' | - tr '_' '-' - ) - - name: Set up Python 3.6 - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - name: Versions - run: | - python3 --version - - name: Checkout Current Repo - uses: actions/checkout@v1 - with: - submodules: true - - name: Checkout tools repo - uses: actions/checkout@v2 - with: - repository: adafruit/actions-ci-circuitpython-libs - path: actions-ci - - name: Install dependencies - # (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.) - run: | - source actions-ci/install.sh - - name: Pip install pylint, Sphinx, pre-commit - run: | - pip install --force-reinstall pylint Sphinx sphinx-rtd-theme pre-commit - - name: Library version - run: git describe --dirty --always --tags - - name: Pre-commit hooks - run: | - pre-commit run --all-files - - name: PyLint - run: | - pylint $( find . -path './adafruit*.py' ) - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace $( find . -path "./examples/*.py" )) - - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - - name: Archive bundles - uses: actions/upload-artifact@v2 - with: - name: bundles - path: ${{ github.workspace }}/bundles/ - - name: Build docs - working-directory: docs - run: sphinx-build -E -W -b html . _build/html - - name: Check For setup.py - id: need-pypi - run: | - echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) - - name: Build Python package - if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') - run: | - pip install --upgrade setuptools wheel twine readme_renderer testresources - python setup.py sdist - python setup.py bdist_wheel --universal - twine check dist/* + - name: Run Build CI workflow + uses: adafruit/workflows-circuitpython-libs/build@main diff --git a/.github/workflows/failure-help-text.yml b/.github/workflows/failure-help-text.yml new file mode 100644 index 0000000..0b1194f --- /dev/null +++ b/.github/workflows/failure-help-text.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2021 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: Failure help text + +on: + workflow_run: + workflows: ["Build CI"] + types: + - completed + +jobs: + post-help: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }} + steps: + - name: Post comment to help + uses: adafruit/circuitpython-action-library-ci-failed@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 6d0015a..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,85 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -name: Release Actions - -on: - release: - types: [published] - -jobs: - upload-release-assets: - runs-on: ubuntu-latest - steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - name: Translate Repo Name For Build Tools filename_prefix - id: repo-name - run: | - echo ::set-output name=repo-name::$( - echo ${{ github.repository }} | - awk -F '\/' '{ print tolower($2) }' | - tr '_' '-' - ) - - name: Set up Python 3.6 - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - name: Versions - run: | - python3 --version - - name: Checkout Current Repo - uses: actions/checkout@v1 - with: - submodules: true - - name: Checkout tools repo - uses: actions/checkout@v2 - with: - repository: adafruit/actions-ci-circuitpython-libs - path: actions-ci - - name: Install deps - run: | - source actions-ci/install.sh - - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - - name: Upload Release Assets - # the 'official' actions version does not yet support dynamically - # supplying asset names to upload. @csexton's version chosen based on - # discussion in the issue below, as its the simplest to implement and - # allows for selecting files with a pattern. - # https://github.com/actions/upload-release-asset/issues/4 - #uses: actions/upload-release-asset@v1.0.1 - uses: csexton/release-asset-action@master - with: - pattern: "bundles/*" - github-token: ${{ secrets.GITHUB_TOKEN }} - - upload-pypi: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Check For setup.py - id: need-pypi - run: | - echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) - - name: Set up Python - if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') - env: - TWINE_USERNAME: ${{ secrets.pypi_username }} - TWINE_PASSWORD: ${{ secrets.pypi_password }} - run: | - python setup.py sdist - twine upload dist/* diff --git a/.github/workflows/release_gh.yml b/.github/workflows/release_gh.yml new file mode 100644 index 0000000..9acec60 --- /dev/null +++ b/.github/workflows/release_gh.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: GitHub Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Run GitHub Release CI workflow + uses: adafruit/workflows-circuitpython-libs/release-gh@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + upload-url: ${{ github.event.release.upload_url }} diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml new file mode 100644 index 0000000..65775b7 --- /dev/null +++ b/.github/workflows/release_pypi.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +name: PyPI Release Actions + +on: + release: + types: [published] + +jobs: + upload-release-assets: + runs-on: ubuntu-latest + steps: + - name: Run PyPI Release CI workflow + uses: adafruit/workflows-circuitpython-libs/release-pypi@main + with: + pypi-username: ${{ secrets.pypi_username }} + pypi-password: ${{ secrets.pypi_password }} diff --git a/.gitignore b/.gitignore index ced7313..db3d538 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,48 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2022 Kattni Rembor, written for Adafruit Industries # -# SPDX-License-Identifier: Unlicense +# SPDX-License-Identifier: MIT +# Do not include files and directories created by your personal work environment, such as the IDE +# you use, except for those already listed here. Pull requests including changes to this file will +# not be accepted. + +# This .gitignore file contains rules for files generated by working with CircuitPython libraries, +# including building Sphinx, testing with pip, and creating a virual environment, as well as the +# MacOS and IDE-specific files generated by using MacOS in general, or the PyCharm or VSCode IDEs. + +# If you find that there are files being generated on your machine that should not be included in +# your git commit, you should create a .gitignore_global file on your computer to include the +# files created by your personal setup. To do so, follow the two steps below. + +# First, create a file called .gitignore_global somewhere convenient for you, and add rules for +# the files you want to exclude from git commits. + +# Second, configure Git to use the exclude file for all Git repositories by running the +# following via commandline, replacing "path/to/your/" with the actual path to your newly created +# .gitignore_global file: +# git config --global core.excludesfile path/to/your/.gitignore_global + +# CircuitPython-specific files +*.mpy + +# Python-specific files __pycache__ -_build *.pyc + +# Sphinx build-specific files +_build + +# This file results from running `pip -e install .` in a local repository +*.egg-info + +# Virtual environment-specific files .env -bundles +.venv + +# MacOS-specific files +*.DS_Store + +# IDE-specific files +.idea +.vscode +*~ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aab5f1c..70ade69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,17 +3,40 @@ # SPDX-License-Identifier: Unlicense repos: -- repo: https://github.com/python/black - rev: stable + - repo: https://github.com/python/black + rev: 23.3.0 hooks: - - id: black -- repo: https://github.com/fsfe/reuse-tool - rev: latest + - id: black + - repo: https://github.com/fsfe/reuse-tool + rev: v1.1.2 hooks: - - id: reuse -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + - id: reuse + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/pycqa/pylint + rev: v2.17.4 + hooks: + - id: pylint + name: pylint (library code) + types: [python] + args: + - --disable=consider-using-f-string + exclude: "^(docs/|examples/|tests/|setup.py$)" + - id: pylint + name: pylint (example code) + description: Run pylint rules on "examples/*.py" files + types: [python] + files: "^examples/" + args: + - --disable=missing-docstring,invalid-name,consider-using-f-string,duplicate-code + - id: pylint + name: pylint (test code) + description: Run pylint rules on "tests/*.py" files + types: [python] + files: "^tests/" + args: + - --disable=missing-docstring,consider-using-f-string,duplicate-code diff --git a/.pylintrc b/.pylintrc index 5c31f66..f945e92 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # # SPDX-License-Identifier: Unlicense @@ -9,11 +9,11 @@ # run arbitrary code extension-pkg-whitelist= -# Add files or directories to the blacklist. They should be base names, not +# Add files or directories to the ignore-list. They should be base names, not # paths. ignore=CVS -# Add files or directories matching the regex patterns to the blacklist. The +# Add files or directories matching the regex patterns to the ignore-list. The # regex matches against base names, not paths. ignore-patterns= @@ -22,12 +22,11 @@ ignore-patterns= #init-hook= # Use multiple processes to speed up Pylint. -# jobs=1 -jobs=2 +jobs=1 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins= +load-plugins=pylint.extensions.no_self_use # Pickle collected data for later comparisons. persistent=yes @@ -55,8 +54,8 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -# disable=import-error,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call -disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,import-error,bad-continuation +# disable=import-error,raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,deprecated-str-translate-call +disable=raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,import-error,pointless-string-statement,unspecified-encoding # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -226,12 +225,6 @@ max-line-length=100 # Maximum number of lines in a module max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no @@ -250,46 +243,30 @@ ignore-comments=yes ignore-docstrings=yes # Ignore imports when computing similarities. -ignore-imports=no +ignore-imports=yes # Minimum lines number of a similarity. -min-similarity-lines=4 +min-similarity-lines=12 [BASIC] -# Naming hint for argument names -argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct argument names argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ -# Naming hint for attribute names -attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct attribute names attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -# Naming hint for class names -# class-name-hint=[A-Z_][a-zA-Z0-9]+$ -class-name-hint=[A-Z_][a-zA-Z0-9_]+$ - # Regular expression matching correct class names # class-rgx=[A-Z_][a-zA-Z0-9]+$ class-rgx=[A-Z_][a-zA-Z0-9_]+$ -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - # Regular expression matching correct constant names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ @@ -297,9 +274,6 @@ const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # ones are exempt. docstring-min-length=-1 -# Naming hint for function names -function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct function names function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ @@ -310,21 +284,12 @@ good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_ # Include a hint for the correct naming format with invalid-name include-naming-hint=no -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ -# Naming hint for method names -method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct method names method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ @@ -340,9 +305,6 @@ no-docstring-rgx=^_ # to this list to register other decorators that produce valid properties. property-classes=abc.abstractproperty -# Naming hint for variable names -variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ - # Regular expression matching correct variable names variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ @@ -434,4 +396,4 @@ min-public-methods=1 # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..88bca9f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +sphinx: + configuration: docs/conf.py + +build: + os: ubuntu-20.04 + tools: + python: "3" + +python: + install: + - requirements: docs/requirements.txt + - requirements: requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index ffa84c4..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: Unlicense - -python: - version: 3 -requirements_file: requirements.txt diff --git a/README.rst b/README.rst index d02128f..453d7c4 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,10 @@ Introduction ============ .. image:: https://readthedocs.org/projects/adafruit-circuitpython-rplidar/badge/?version=latest - :target: https://circuitpython.readthedocs.io/projects/rplidar/en/latest/ + :target: https://docs.circuitpython.org/projects/rplidar/en/latest/ :alt: Documentation Status -.. image:: https://img.shields.io/discord/327254708534116352.svg +.. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg :target: https://adafru.it/discord :alt: Discord @@ -13,10 +13,16 @@ Introduction :target: https://github.com/adafruit/Adafruit_CircuitPython_RPLIDAR :alt: Build Status +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code Style: Black + .. Provide a convenient interface to the Slamtec RPLidar. Dependencies ============= + +Install with PyPy: ``pip install Adafruit_CircuitPython_RPLIDAR`` This driver depends on: * `Adafruit CircuitPython `_ @@ -33,33 +39,24 @@ Usage Example .. code-block:: python import os - from math import cos, sin, pi, floor - import pygame - from adafruit_circuitpython_rplidar import RPLidar - - # Set up pygame and the display - os.putenv('SDL_FBDEV', '/dev/fb1') - pygame.init() - lcd = pygame.display.set_mode((320,240)) - pygame.mouse.set_visible(False) - lcd.fill((0,0,0)) - pygame.display.update() + from math import floor + from adafruit_rplidar import RPLidar + # Setup the RPLidar PORT_NAME = '/dev/ttyUSB0' - lidar = RPLidar(None, PORT_NAME) + lidar = RPLidar(None, PORT_NAME, timeout=3) # used to scale data to fit on the screen max_distance = 0 def process_data(data): - # Do something useful with the data - pass + print(data) scan_data = [0]*360 try: - print(lidar.get_info()) + # print(lidar.get_info()) for scan in lidar.iter_scans(): for (_, angle, distance) in scan: scan_data[min([359, floor(angle)])] = distance @@ -71,14 +68,16 @@ Usage Example lidar.disconnect() +Documentation +============= + +API documentation for this library can be found on `Read the Docs `_. + +For information on building library documentation, please check out `this guide `_. + Contributing ============ Contributions are welcome! Please read our `Code of Conduct -`_ +`_ before contributing to help this project stay welcoming. - -Documentation -============= - -For information on building library documentation, please check out `this guide `_. diff --git a/adafruit_rplidar.py b/adafruit_rplidar.py index d7b6d8f..9b12340 100644 --- a/adafruit_rplidar.py +++ b/adafruit_rplidar.py @@ -11,6 +11,7 @@ * Author(s): Dave Astels * Based on https://github.com/SkoltechRobotics/rplidar by Artyom Pavlov +* and updates from https://github.com/Roboticia/RPLidar by Julien JEHL Implementation Notes -------------------- @@ -23,18 +24,26 @@ * Adafruit CircuitPython firmware for the supported boards: https://github.com/adafruit/circuitpython/releases -Version 0.0.1 does NOT support CircuitPython. Future versions will. +The Current Version does NOT support CircuitPython. Future versions will. """ import struct import sys import time import warnings +from collections import namedtuple + +try: + from typing import Tuple, Dict, Any, Optional, List, Iterator, Union + from busio import UART + from digitalio import DigitalInOut +except ImportError: + pass # pylint:disable=invalid-name,undefined-variable,global-variable-not-assigned -# pylint:disable=too-many-arguments,raise-missing-from +# pylint:disable=too-many-arguments,raise-missing-from,too-many-instance-attributes -__version__ = "0.0.1-auto.0" +__version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RPLIDAR.git" SYNC_BYTE = b"\xA5" @@ -46,16 +55,12 @@ STOP_BYTE = b"\x25" RESET_BYTE = b"\x40" -SCAN_BYTE = b"\x20" -FORCE_SCAN_BYTE = b"\x21" - DESCRIPTOR_LEN = 7 INFO_LEN = 20 HEALTH_LEN = 3 INFO_TYPE = 4 HEALTH_TYPE = 6 -SCAN_TYPE = 129 # Constants & Command to start A2 motor MAX_MOTOR_PWM = 1023 @@ -68,12 +73,24 @@ 2: "Error", } +SCAN_TYPE_NORMAL = 0 +SCAN_TYPE_FORCE = 1 +SCAN_TYPE_EXPRESS = 2 + +_SCAN_TYPES = ( + {"byte": b"\x20", "response": 129, "size": 5}, + {"byte": b"\x21", "response": 129, "size": 5}, + {"byte": b"\x82", "response": 130, "size": 84}, +) + +express_packet = namedtuple("express_packet", "distance angle new_scan start_angle") + class RPLidarException(Exception): """Basic exception class for RPLidar""" -def _process_scan(raw): +def _process_scan(raw: bytes) -> Tuple[bool, int, float, float]: """Processes input raw data and returns measurement data""" new_scan = bool(raw[0] & 0b1) inversed_new_scan = bool((raw[0] >> 1) & 0b1) @@ -88,6 +105,19 @@ def _process_scan(raw): return new_scan, quality, angle, distance +def _process_express_scan( + data: "ExpressPacket", new_angle: float, frame: int +) -> Tuple[bool, None, float, float]: + new_scan = (new_angle < data.start_angle) & (frame == 1) + angle = ( + data.start_angle + + ((new_angle - data.start_angle) % 360) / 32 * frame + - data.angle[frame - 1] + ) % 360 + distance = data.distance[frame - 1] + return new_scan, None, angle, distance + + class RPLidar: """Class for communicating with RPLidar rangefinder scanners""" @@ -97,19 +127,35 @@ class RPLidar: timeout = 1 #: Serial port timeout motor = False #: Is motor running? baudrate = 115200 #: Baudrate for serial port - - def __init__(self, motor_pin, port, baudrate=115200, timeout=1, logging=False): + scanning = False + descriptor_size = 0 + scan_type = SCAN_TYPE_NORMAL + express_frame = 32 + express_data = False + express_old_data = None + + def __init__( + self, + motor_pin: DigitalInOut, + port: UART, + baudrate: int = 115200, + timeout: float = 1, + logging: bool = False, + ) -> None: """Initialize RPLidar object for communicating with the sensor. Parameters + motor_pin : digitalio.DigitalInOut + Pin controlling the motor port : busio.UART or str Serial port instance or name of the port to which the sensor is connected baudrate : int, optional Baudrate for serial connection (the default is 115200) timeout : float, optional Serial port connection timeout in seconds (the default is 1) - logging : whether to output logging information + logging : bool, optional + Whether to output logging information """ self.motor_pin = motor_pin self.port = port @@ -129,17 +175,17 @@ def __init__(self, motor_pin, port, baudrate=115200, timeout=1, logging=False): self.connect() self.start_motor() - def log(self, level, msg): + def log(self, level: str, msg: str) -> None: """Output the level and a message if logging is enabled.""" if self.logging: sys.stdout.write("{0}: {1}\n".format(level, msg)) - def log_bytes(self, level, msg, ba): + def log_bytes(self, level: str, msg: str, ba: bytes) -> None: """Log and output a byte array in a readable way.""" bs = ["%02x" % b for b in ba] self.log(level, msg + " ".join(bs)) - def connect(self): + def connect(self) -> None: """Connects to the serial port named by the port instance var. If it was connected to another serial port disconnects from it first.""" if not self.is_CP: @@ -152,33 +198,32 @@ def connect(self): parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=self.timeout, - dsrdtr=True, ) except serial.SerialException as err: raise RPLidarException( "Failed to connect to the sensor " "due to: %s" % err ) - def disconnect(self): + def disconnect(self) -> None: """Disconnects from the serial port""" if self._serial_port is None: return self._serial_port.close() - def set_pwm(self, pwm): + def set_pwm(self, pwm: int) -> None: """Set the motor PWM""" assert 0 <= pwm <= MAX_MOTOR_PWM payload = struct.pack(" None: """Manipulate the motor""" if self.is_CP: self.motor_pin.value = val else: self._serial_port.dtr = not val - def start_motor(self): + def start_motor(self) -> None: """Starts sensor motor""" self.log("info", "Starting motor") # For A1 @@ -188,7 +233,7 @@ def start_motor(self): self.set_pwm(DEFAULT_MOTOR_PWM) self.motor_running = True - def stop_motor(self): + def stop_motor(self) -> None: """Stops sensor motor""" self.log("info", "Stopping motor") # For A2 @@ -198,7 +243,7 @@ def stop_motor(self): self._control_motor(False) self.motor_running = False - def _send_payload_cmd(self, cmd, payload): + def _send_payload_cmd(self, cmd: bytes, payload: bytes) -> None: """Sends `cmd` command with `payload` to the sensor""" size = struct.pack("B", len(payload)) req = SYNC_BYTE + cmd + size + payload @@ -209,13 +254,13 @@ def _send_payload_cmd(self, cmd, payload): self._serial_port.write(req) self.log_bytes("debug", "Command sent: ", req) - def _send_cmd(self, cmd): + def _send_cmd(self, cmd: bytes) -> None: """Sends `cmd` command to the sensor""" req = SYNC_BYTE + cmd self._serial_port.write(req) self.log_bytes("debug", "Command sent: ", req) - def _read_descriptor(self): + def _read_descriptor(self) -> Tuple[int, bool, int]: """Reads descriptor packet""" descriptor = self._serial_port.read(DESCRIPTOR_LEN) self.log_bytes("debug", "Received descriptor:", descriptor) @@ -226,7 +271,7 @@ def _read_descriptor(self): is_single = descriptor[-2] == 0 return descriptor[2], is_single, descriptor[-1] - def _read_response(self, dsize): + def _read_response(self, dsize: int) -> bytes: """Reads response packet with length of `dsize` bytes""" self.log("debug", "Trying to read response: %d bytes" % dsize) data = self._serial_port.read(dsize) @@ -236,7 +281,7 @@ def _read_response(self, dsize): return data @property - def info(self): + def info(self) -> Dict[str, Any]: """Get device information Returns @@ -253,7 +298,7 @@ def info(self): if dtype != INFO_TYPE: raise RPLidarException("Wrong response data type") raw = self._read_response(dsize) - serialnumber_bytes = struct.unpack("BBBBBBBBBBBBBBBB", raw[4:]) + serialnumber_bytes = struct.unpack("B" * len(raw[4:]), raw[4:]) serialnumber = "".join(reversed(["%02x" % b for b in serialnumber_bytes])) data = { "model": raw[0], @@ -264,7 +309,7 @@ def info(self): return data @property - def health(self): + def health(self) -> Tuple[str, int]: """Get device health state. When the core system detects some potential risk that may cause hardware failure in the future, the returned status value will be 'Warning'. But sensor can still work @@ -292,40 +337,102 @@ def health(self): error_code = (raw[1] << 8) + raw[2] return (status, error_code) - def clear_input(self): + def clear_input(self) -> None: """Clears input buffer by reading all available data""" + if self.scanning: + raise RPLidarException("Clearing not allowed during active scanning!") + self._serial_port.flushInput() + self.express_frame = 32 + self.express_data = False + + def start(self, scan_type: int = SCAN_TYPE_NORMAL) -> None: + """Start the scanning process + + Parameters + + scan_type : int, optional + Normal, force or express; default is normal + """ + if self.scanning: + raise RPLidarException("Scanning already running!") + # Start the scanning process, enable laser diode and the + # measurement system + status, error_code = self.health + self.log("debug", "Health status: %s [%d]" % (status, error_code)) + if status == _HEALTH_STATUSES[2]: + self.log( + "warning", + "Trying to reset sensor due to the error. " + "Error code: %d" % (error_code), + ) + self.reset() + status, error_code = self.health + if status == _HEALTH_STATUSES[2]: + raise RPLidarException( + "RPLidar hardware failure. " "Error code: %d" % error_code + ) + elif status == _HEALTH_STATUSES[1]: + self.log( + "warning", + "Warning sensor status detected! " "Error code: %d" % (error_code), + ) + cmd = _SCAN_TYPES[scan_type]["byte"] + self.log("info", "starting scan process in %s mode" % scan_type) - def stop(self): + if scan_type == "express": + self._send_payload_cmd(cmd, b"\x00\x00\x00\x00\x00") + else: + self._send_cmd(cmd) + + dsize, is_single, dtype = self._read_descriptor() + if dsize != _SCAN_TYPES[scan_type]["size"]: + raise RPLidarException("Wrong info reply length") + if is_single: + raise RPLidarException("Not a multiple response mode") + if dtype != _SCAN_TYPES[scan_type]["response"]: + raise RPLidarException("Wrong response data type") + self.descriptor_size = dsize + self.scan_type = scan_type + self.scanning = True + + def stop(self) -> None: """Stops scanning process, disables laser diode and the measurement system, moves sensor to the idle state.""" self.log("info", "Stopping scanning") self._send_cmd(STOP_BYTE) time.sleep(0.001) + self.scanning = False self.clear_input() - def reset(self): + def reset(self) -> None: """Resets sensor core, reverting it to a similar state as it has just been powered up.""" self.log("info", "Resetting the sensor") self._send_cmd(RESET_BYTE) time.sleep(0.002) + self.clear_input() - def iter_measurements(self, max_buf_meas=500): + def iter_measurements( + self, max_buf_meas: int = 500, scan_type: int = SCAN_TYPE_NORMAL + ) -> Iterator[Tuple[bool, Optional[int], float, float]]: """Iterate over measurements. Note that consumer must be fast enough, otherwise data will be accumulated inside buffer and consumer will get data with increasing lag. Parameters - max_buf_meas : int + max_buf_meas : int, optional Maximum number of measurements to be stored inside the buffer. Once - number exceeds this limit buffer will be emptied out. + number exceeds this limit buffer will be emptied out. Default is + 500. + scan_type : int, optional + Normal, force or express; default is normal Yields new_scan : bool True if measurement belongs to a new scan - quality : int + quality : int | None Reflected laser pulse strength angle : float The measurement heading angle in degree unit [0, 360) @@ -334,37 +441,11 @@ def iter_measurements(self, max_buf_meas=500): In millimeter unit. Set to 0 when measurement is invalid. """ self.start_motor() - status, error_code = self.health - self.log("debug", "Health status: %s [%d]" % (status, error_code)) - if status == _HEALTH_STATUSES[2]: - self.log( - "warning", - "Trying to reset sensor due to the error. " - "Error code: %d" % (error_code), - ) - self.reset() - status, error_code = self.health - if status == _HEALTH_STATUSES[2]: - raise RPLidarException( - "RPLidar hardware failure. " "Error code: %d" % error_code - ) - elif status == _HEALTH_STATUSES[1]: - self.log( - "warning", - "Warning sensor status detected! " "Error code: %d" % (error_code), - ) - cmd = SCAN_BYTE - self._send_cmd(cmd) - dsize, is_single, dtype = self._read_descriptor() - if dsize != 5: - raise RPLidarException("Wrong info reply length") - if is_single: - raise RPLidarException("Not a multiple response mode") - if dtype != SCAN_TYPE: - raise RPLidarException("Wrong response data type") + if not self.scanning: + self.start(scan_type) + while True: - raw = self._read_response(dsize) - self.log_bytes("debug", "Received scan response: ", raw) + dsize = self.descriptor_size if max_buf_meas: data_in_buf = self._serial_port.in_waiting if data_in_buf > max_buf_meas * dsize: @@ -374,9 +455,54 @@ def iter_measurements(self, max_buf_meas=500): "Clearing buffer..." % (data_in_buf // dsize, max_buf_meas), ) self._serial_port.read(data_in_buf // dsize * dsize) - yield _process_scan(raw) + if self.scan_type == SCAN_TYPE_NORMAL: + raw = self._read_response(dsize) + self.log_bytes("debug", "Received scan response: ", raw) + yield _process_scan(raw) + elif self.scan_type == SCAN_TYPE_EXPRESS: + if self.express_frame == 32: + self.express_frame = 0 + if not self.express_data: + self.log("debug", "reading first time bytes") + self.express_data = ExpressPacket.from_string( + self._read_response(dsize) + ) + + self.express_old_data = self.express_data + self.log( + "debug", + "set old_data with start_angle %f" + % self.express_old_data.start_angle, + ) + self.express_data = ExpressPacket.from_string( + self._read_response(dsize) + ) + self.log( + "debug", + "set new_data with start_angle %f" + % self.express_data.start_angle, + ) + + self.express_frame += 1 + self.log( + "debug", + "process scan of frame %d with angle : " + "%f and angle new : %f" + % ( + self.express_frame, + self.express_old_data.start_angle, + self.express_data.start_angle, + ), + ) + yield _process_express_scan( + self.express_old_data, + self.express_data.start_angle, + self.express_frame, + ) - def iter_measurments(self, max_buf_meas=500): + def iter_measurments( + self, max_buf_meas: int = 500 + ) -> Iterator[Tuple[bool, int, float, float]]: """For compatibility, this method wraps `iter_measurements`""" warnings.warn( "The method `iter_measurments` has been renamed " @@ -385,18 +511,22 @@ def iter_measurments(self, max_buf_meas=500): ) self.iter_measurements(max_buf_meas=max_buf_meas) - def iter_scans(self, max_buf_meas=500, min_len=5): + def iter_scans( + self, max_buf_meas: int = 500, min_len: int = 5 + ) -> List[Union[int, float]]: """Iterate over scans. Note that consumer must be fast enough, otherwise data will be accumulated inside buffer and consumer will get data with increasing lag. Parameters - max_buf_meas : int + max_buf_meas : int, optional Maximum number of measurements to be stored inside the buffer. Once - number exceeds this limit buffer will be emptied out. - min_len : int + number exceeds this limit buffer will be emptied out. Default is + 500. + min_len : int, optional Minimum number of measurements in the scan for it to be yielded. + Default is 5. Yields @@ -414,3 +544,44 @@ def iter_scans(self, max_buf_meas=500, min_len=5): scan = [] if quality > 0 and distance > 0: scan.append((quality, angle, distance)) + + +class ExpressPacket(express_packet): + """Class representing a Express type Packet""" + + sync1 = 0xA + sync2 = 0x5 + sign = {0: 1, 1: -1} + + @classmethod + def from_string(cls, data: bytes) -> "ExpressPacket": + """Decode and Instantiate the class from a string packet""" + packet = bytearray(data) + + if (packet[0] >> 4) != cls.sync1 or (packet[1] >> 4) != cls.sync2: + raise ValueError("try to parse corrupted data ({})".format(packet)) + + checksum = 0 + for b in packet[2:]: + checksum ^= b + if checksum != (packet[0] & 0b00001111) + ((packet[1] & 0b00001111) << 4): + raise ValueError("Invalid checksum ({})".format(packet)) + + new_scan = packet[3] >> 7 + start_angle = (packet[2] + ((packet[3] & 0b01111111) << 8)) / 64 + + d = a = () + for i in range(0, 80, 5): + d += ((packet[i + 4] >> 2) + (packet[i + 5] << 6),) + a += ( + ((packet[i + 8] & 0b00001111) + ((packet[i + 4] & 0b00000001) << 4)) + / 8 + * cls.sign[(packet[i + 4] & 0b00000010) >> 1], + ) + d += ((packet[i + 6] >> 2) + (packet[i + 7] << 6),) + a += ( + ((packet[i + 8] >> 4) + ((packet[i + 6] & 0b00000001) << 4)) + / 8 + * cls.sign[(packet[i + 6] & 0b00000010) >> 1], + ) + return cls(d, a, new_scan, start_angle) diff --git a/docs/conf.py b/docs/conf.py index fb61e04..fcc75ec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,6 +6,7 @@ import os import sys +import datetime sys.path.insert(0, os.path.abspath("..")) @@ -16,6 +17,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinxcontrib.jquery", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.todo", @@ -29,16 +31,16 @@ intersphinx_mapping = { - "python": ("https://docs.python.org/3.4", None), + "python": ("https://docs.python.org/3", None), "BusDevice": ( - "https://circuitpython.readthedocs.io/projects/busdevice/en/latest/", + "https://docs.circuitpython.org/projects/busdevice/en/latest/", None, ), "Register": ( - "https://circuitpython.readthedocs.io/projects/register/en/latest/", + "https://docs.circuitpython.org/projects/register/en/latest/", None, ), - "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None), + "CircuitPython": ("https://docs.circuitpython.org/en/latest/", None), } # Add any paths that contain templates here, relative to this directory. @@ -51,7 +53,14 @@ # General information about the project. project = "Adafruit_circuitpython RPLIDAR Library" -copyright = "2019 Dave Astels" +creation_year = "2019" +current_year = str(datetime.datetime.now().year) +year_duration = ( + current_year + if current_year == creation_year + else creation_year + " - " + current_year +) +copyright = year_duration + " Dave Astels" author = "Dave Astels" # The version info for the project you're documenting, acts as replacement for @@ -68,7 +77,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -100,19 +109,9 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only import and set the theme if we're building docs locally - try: - import sphinx_rtd_theme - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] - except: - html_theme = "default" - html_theme_path = ["."] -else: - html_theme_path = ["."] +import sphinx_rtd_theme + +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/index.rst b/docs/index.rst index a5aaf03..962c93a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,8 +32,9 @@ Table of Contents .. toctree:: :caption: Other Links - Download - CircuitPython Reference Documentation + Download from GitHub + Download Library Bundle + CircuitPython Reference Documentation CircuitPython Support Forum Discord Chat Adafruit Learning System diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..979f568 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +sphinx +sphinxcontrib-jquery +sphinx-rtd-theme diff --git a/examples/rplidar_simpletest.py b/examples/rplidar_simpletest.py index e69de29..435b5c6 100644 --- a/examples/rplidar_simpletest.py +++ b/examples/rplidar_simpletest.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +from math import floor +from adafruit_rplidar import RPLidar + +# Setup the RPLidar +PORT_NAME = "/dev/ttyUSB0" +lidar = RPLidar(None, PORT_NAME, timeout=3) + +# used to scale data to fit on the screen +max_distance = 0 + + +def process_data(data): + print(data) + + +scan_data = [0] * 360 + +try: + # print(lidar.get_info()) + for scan in lidar.iter_scans(): + for _, angle, distance in scan: + scan_data[min([359, floor(angle)])] = distance + process_data(scan_data) + +except KeyboardInterrupt: + print("Stopping.") +lidar.stop() +lidar.disconnect() diff --git a/optional_requirements.txt b/optional_requirements.txt new file mode 100644 index 0000000..d4e27c4 --- /dev/null +++ b/optional_requirements.txt @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8ce49ca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +[build-system] +requires = [ + "setuptools", + "wheel", + "setuptools-scm", +] + +[project] +name = "adafruit-circuitpython-rplidar" +description = "RPLidar support" +version = "0.0.0+auto.0" +readme = "README.rst" +authors = [ + {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} +] +urls = {Homepage = "https://github.com/adafruit/Adafruit_CircuitPython_rplidar"} +keywords = [ + "adafruit", + "blinka", + "circuitpython", + "micropython", + "rplidar", + "lidar", + "sensors", +] +license = {text = "MIT"} +classifiers = [ + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Embedded Systems", + "Topic :: System :: Hardware", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] +dynamic = ["dependencies", "optional-dependencies"] + +[tool.setuptools] +py-modules = ["adafruit_rplidar"] + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} +optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} diff --git a/requirements.txt b/requirements.txt index f162505..fcef575 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries # # SPDX-License-Identifier: Unlicense Adafruit-Blinka -adafruit-circuitpython-busdevice adafruit-circuitpython-register +adafruit-circuitpython-busdevice diff --git a/setup.py b/setup.py deleted file mode 100644 index d30f07f..0000000 --- a/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -"""A setuptools based setup module. - -See: -https://packaging.python.org/en/latest/distributing.html -https://github.com/pypa/sampleproject -""" - -from setuptools import setup, find_packages - -# To use a consistent encoding -from codecs import open -from os import path - -here = path.abspath(path.dirname(__file__)) - -# Get the long description from the README file -with open(path.join(here, "README.rst"), encoding="utf-8") as f: - long_description = f.read() - -setup( - name="adafruit-circuitpython-rplidar", - use_scm_version=True, - setup_requires=["setuptools_scm"], - description="RPLidar support", - long_description=long_description, - long_description_content_type="text/x-rst", - # The project's main homepage. - url="https://github.com/adafruit/Adafruit_CircuitPython_rplidar", - # Author details - author="Adafruit Industries", - author_email="circuitpython@adafruit.com", - install_requires=["Adafruit-Blinka"], - # Choose your license - license="MIT", - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries", - "Topic :: System :: Hardware", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - ], - # What does your project relate to? - keywords="adafruit blinka circuitpython micropython rplidar lidar sensors", - # You can just specify the packages manually here if your project is - # simple. Or you can use find_packages(). - # TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER, - # CHANGE `py_modules=['...']` TO `packages=['...']` - py_modules=["adafruit_rplidar"], -)