diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..2b21548 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,16 @@ +[bumpversion] +current_version = 1.0.0 +commit = False +tag = False + +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + +[bumpversion:file:src/actuonix_lac/__init__.py] +search = __version__ = "{current_version}" +replace = __version__ = "{new_version}" + +[bumpversion:file:tests/test_actuonix_lac.py] +search = __version__ == "{current_version}" +replace = __version__ == "{new_version}" diff --git a/.gitignore b/.gitignore index 1e2bcb3..f2e3745 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,135 @@ +# IDE settings +.idea + +# pytest artefakts +pytest*.xml + +# Byte-compiled / optimized / DLL files __pycache__/ -*.egg-info +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d12230f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,55 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.7 +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: no-commit-to-branch + - id: check-executables-have-shebangs + - id: check-ast + - id: check-merge-conflict + - id: check-toml + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-case-conflict + - id: check-json + - id: check-symlinks + - id: pretty-format-json + args: + - --autofix +- repo: https://github.com/psf/black + rev: stable + hooks: + - id: black + language_version: python3.7 +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.790 + hooks: + - id: mypy + language: system + args: [--strict] #, --ignore-missing-imports] +- repo: https://github.com/pre-commit/mirrors-pylint + rev: v2.4.4 + hooks: + - id: pylint + language: system +- repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.1.7 + hooks: + - id: forbid-crlf + - id: remove-crlf + - id: forbid-tabs + - id: remove-tabs +- repo: https://github.com/PyCQA/bandit + rev: 1.6.2 + hooks: + - id: bandit + args: ["--skip=B101"] +- repo: git://github.com/Lucas-C/pre-commit-hooks-markup + rev: v1.0.0 + hooks: + - id: rst-linter diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..999d6cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2019 Jerimiah Daley and Richard Emile Sarkis +Copyright (c) 2020 Eero af Heurlin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 4e05bba..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# actuonix-lac -Python module for controlling an Actuonix Linear Actuator Control Board (https://www.actuonix.com/LAC-Board-p/lac.htm). - -There's *probably* other APIs out there for the same device, and they're *probaby* better than this one, and they're *probably* written in C, but we made one anyways. Too bad we had to use Python instead of HolyC, but meh; it's fine diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..bb04ab2 --- /dev/null +++ b/README.rst @@ -0,0 +1,32 @@ +============ +actuonix-lac +============ + +Python module for controlling an Actuonix/Firgelli Linear Actuator Control Board (https://www.actuonix.com/LAC-Board-p/lac.htm). + +There's both blocking and asyncio variant available, usage:: + + from actuonix_lac.blocking import LAC # The old from actuonix_lac.lac import LAC also works + sb = LAC() + sb.set_position(255) + # wait a moment for the movement to complete + sb.get_feedback() + +or:: + + import asyncio + from actuonix_lac.aio import AsyncLAC + loop = asyncio.get_event_loop() + s = AsyncLAC() + loop.run_until_complete(s.set_position, 500) + # wait a moment for the movement to complete + loop.run_until_complete(s.get_feedback()) + +Obviously when inside an async function just use await instead of loop.run_until_complete. + +There is also CLI interface (but due to USB init taking some time you should not use it in loops etc):: + + laccontrol get-position # returns the raw position + laccontrol set-position 316 # sets target position to 316 + +Remember that you must install the extra feature "cli" (or "all") to use the CLI entrypoint. diff --git a/actuonix_lac/lac.py b/actuonix_lac/lac.py deleted file mode 100644 index 823c468..0000000 --- a/actuonix_lac/lac.py +++ /dev/null @@ -1,170 +0,0 @@ -import struct -import usb.core -import time - -class LAC: - SET_ACCURACY = 0x01 - SET_RETRACT_LIMIT = 0x02 - SET_EXTEND_LIMIT = 0x03 - SET_MOVEMENT_THRESHOLD = 0x04 - SET_STALL_TIME = 0x05 - SET_PWM_THRESHOLD = 0x06 - SET_DERIVATIVE_THRESHOLD = 0x07 - SET_MAX_DERIVATIVE = 0x08 - SET_MIN_DERIVATIVE = 0x09 - SET_MAX_PWM_VALUE = 0x0A - SET_MIN_PWM_VALUE = 0x0B - SET_Kp = 0x0C - SET_Kd = 0x0D - SET_AVERAGE_RC = 0x0E - SET_AVERAGE_ADC = 0x0F - - GET_FEEDBACK = 0x10 - - SET_POSITION = 0x20 - SET_SPEED = 0x21 - - DISABLE_MANUAL = 0x30 - - RESET = 0xFF - - def __init__(self, vendorID=0x4D8, productID=0xFC5F): - self.device = usb.core.find(idVendor=vendorID, idProduct=productID) # Defaults for our LAC; give yours a test - if self.device is None: - raise Exception("No board found, ensure board is connected and powered and matching the IDs provided") - - self.device.set_configuration() - - # Take data and send it to LAC - def send_data(self, function, value=0): - if value < 0 or value > 1023: - raise ValueError("Value is OOB. Must be 2-byte integer in rage [0, 1023]") - - data = struct.pack(b'BBB', function, value & 0xFF, (value & 0xFF00) >> 8) # Low byte masked in, high byte masked and moved down - self.device.write(1, data, 100) # Magic numbers from the PyUSB tutorial - time.sleep(.05) # Just to be sure it's all well and sent - response = self.device.read(0x81, 3, 100) # 3 because there's three bytes to a packet - return (response[2] << 8) + response[1] # High byte moved left, then tack on the low byte - - # How close to target distance is accepted - # value/1024 * stroke gives distance, where stroke is max - # extension length (all values in mm). Round to nearest - # integer - def set_accuracy(self, value=4): - self.send_data(self.SET_ACCURACY, value) - - # How far back the actuator can go. A value - # of 0 hits the mechanical stop, but this - # is not recommended. The value you want to send - # is calculated by (distance * 1023)/stroke where - # distance is intended distance and stroke is max - # extension length, all values in mm. Round to - # nearest integer - def set_retract_limit(self, value): - self.send_data(self.SET_RETRACT_LIMIT, value) - - # How far forward the actuator can go. A value - # of 1023 hits the mechanical stop, but this - # is not recommended. See above for math - def set_extend_limit(self, value): - self.send_data(self.SET_EXTEND_LIMIT, value) - - # Minimum speed before actuator is considered stalling - def set_movement_threshold(self, value): - self.send_data(self.SET_MOVEMENT_THRESHOLD, value) - - # Timeout (ms) before actuator shuts off after stalling - def set_stall_time(self, value): - self.send_data(self.SET_STALL_TIME, value) - - # When feedback-set>this, set speed to maximum - def set_pwm_threshold(self, value): - self.send_data(self.SET_PWM_THRESHOLD, value) - - # Compared to measured speed to determine - # PWM increase (prevents stalls). Normally - # equal to movement threshold - def set_derivative_threshold(self, value): - self.send_data(self.SET_DERIVATIVE_THRESHOLD, value) - - # Maximum value the D term can contribute to control speed - def set_max_derivative(self, value): - self.send_data(self.SET_MAX_DERIVATIVE, value) - - # Minimum value the D term can contribute to control speed - def set_min_derivative(self, value): - self.send_data(self.SET_MIN_DERIVATIVE, value) - - # Speed the actuator runs at when outside the pwm threshold - # 1023 enables top speed, though actuator may try to move - # faster to avoid stalling - def set_max_pwm_value(self, value): - self.send_data(self.SET_MAX_PWM_VALUE, value) - - # Minimum PWM value applied by PD - def set_min_pwm_value(self, value): - self.send_data(self.SET_MIN_PWM_VALUE, value) - - # Higher value = faster approach to target, but also more - # overshoot - def set_proportional_gain(self, value): - self.send_data(self.SET_PROPORTIONAL_GAIN, value) - - # Rate at which differential portion of controller increases - # while stalling. Not a /real/ differential term, but - # similar effect. When stalling, derivtive term is - # incremented to attempt escape - def set_derivative_gain(self, value): - self.send_data(self.SET_DERIVATIVE_GAIN, value) - - # Number of samples used in filtering the RC input signal - # before the actuator moves. High value = more stability, - # but lower response time. value * 20ms = delay time. - # This does NOT affect filter feedback delay; control - # response to valid input signals is unaffected - def set_average_rc(self, value=4): - self.send_data(self.SET_AVERAGE_RC, value) - - # Number of samples used in filtering the feedback and analog - # input signals, if active. Similar delay effect to - # set_average_rc, but this DOES affect control response. PD - # loop values may need to be retuned when adjusting this - def set_average_adc(self, value): - self.send_data(self.SET_AVERAGE_ADC, value) - - - # Causes actuator to send a feedback packet containing its - # current position. This is read directly from ADC and might - # not be equal to the set point if yet unreached - def get_feedback(self): - return self.send_data(self.GET_FEEDBACK) - - - # Set the LAC's position. This shouldn't be shocking, given - # like ya know the name of the function? Note that this will - # disable RC, I, and V inputs until reboot. To know what - # number to send, do (distance * 1023)/stroke where distance - # is intended position as a distance from the back hardstop, - # in mm, and stroke is the maximum length of extension, in mm. - # Be sure to round your result to the nearest integer! - def set_position(self, value): - self.send_data(self.SET_POSITION, value) - - # This command is not documented, but it's probably - # easy to infer and just guess via trial by fire - def set_speed(self, value): - self.send_data(self.SET_SPEED, value) - - - # Saves current config to EEPROM and disables all four - # potentiometers. On reboot, these values will continue being used - # instead of the potentiometer values. Analog inputs function - # as normal either way - def disable_manual(self): - self.send_data(self.DISABLE_MANUAL) - - - # Enables manual control potentiometers and resets config - # to factory default - def reset(self): - self.send_data(self.RESET) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..7167fc4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,793 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "aspy.yaml" +version = "1.3.0" +description = "A few extensions to pyyaml." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "astroid" +version = "2.4.2" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0,<1.5.0" +six = ">=1.12,<2.0" +typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} +wrapt = ">=1.11,<2.0" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "20.3.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "bandit" +version = "1.6.2" +description = "Security oriented static analyser for python code." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=3.13" +six = ">=1.10.0" +stevedore = ">=1.20.0" + +[[package]] +name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +name = "bump2version" +version = "0.5.11" +description = "Version-bump your software with a single command!" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "cfgv" +version = "3.2.0" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "distlib" +version = "0.3.1" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "gitdb" +version = "4.0.5" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +smmap = ">=3.0.1,<4" + +[[package]] +name = "gitpython" +version = "3.1.11" +description = "Python Git Library" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "identify" +version = "1.5.9" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.extras] +license = ["editdistance"] + +[[package]] +name = "importlib-metadata" +version = "2.0.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.6.4" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.4.3" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy" +version = "0.790" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nodeenv" +version = "1.5.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "20.4" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +name = "pathspec" +version = "0.8.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pbr" +version = "5.5.1" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "pre-commit" +version = "1.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +"aspy.yaml" = "*" +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = "*" +six = "*" +toml = "*" +virtualenv = ">=15.2" + +[[package]] +name = "py" +version = "1.9.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pylint" +version = "2.6.0" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.5.*" + +[package.dependencies] +astroid = ">=2.4.0,<=2.5" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +checkqa_mypy = ["mypy (==0.780)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.14.0" +description = "Pytest support for asyncio." +category = "dev" +optional = false +python-versions = ">= 3.5" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pyusb" +version = "1.1.0" +description = "Python USB access module" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "regex" +version = "2020.10.28" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "3.0.4" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "stevedore" +version = "3.2.2" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typed-ast" +version = "1.4.1" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "virtualenv" +version = "20.1.0" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +importlib-metadata = {version = ">=0.12,<3", markers = "python_version < \"3.8\""} +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[[package]] +name = "wrapt" +version = "1.12.1" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.4.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[extras] +all = ["click"] +cli = ["click"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "ddc7d9e0ea2049e2c05a67d4ad9dc7a7e4751c8ba42ac85a784b7efd5668c963" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +"aspy.yaml" = [ + {file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"}, + {file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"}, +] +astroid = [ + {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, + {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, +] +bandit = [ + {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, + {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, +] +black = [ + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +bump2version = [ + {file = "bump2version-0.5.11-py2.py3-none-any.whl", hash = "sha256:bfcc051498dda9fd9ac8634689f4516e1c20fdeeace3278932cc6e1248418b36"}, + {file = "bump2version-0.5.11.tar.gz", hash = "sha256:524bde030318fe2543038defe0f77739605636fef96924883813cb290cf79c1e"}, +] +cfgv = [ + {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, + {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +distlib = [ + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +gitdb = [ + {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"}, + {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"}, +] +gitpython = [ + {file = "GitPython-3.1.11-py3-none-any.whl", hash = "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b"}, + {file = "GitPython-3.1.11.tar.gz", hash = "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"}, +] +identify = [ + {file = "identify-1.5.9-py2.py3-none-any.whl", hash = "sha256:5dd84ac64a9a115b8e0b27d1756b244b882ad264c3c423f42af8235a6e71ca12"}, + {file = "identify-1.5.9.tar.gz", hash = "sha256:c9504ba6a043ee2db0a9d69e43246bc138034895f6338d5aed1b41e4a73b1513"}, +] +importlib-metadata = [ + {file = "importlib_metadata-2.0.0-py2.py3-none-any.whl", hash = "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"}, + {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, + {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy = [ + {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"}, + {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"}, + {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"}, + {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"}, + {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"}, + {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"}, + {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"}, + {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"}, + {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"}, + {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"}, + {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"}, + {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"}, + {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"}, + {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nodeenv = [ + {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, + {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pathspec = [ + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, +] +pbr = [ + {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"}, + {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +pre-commit = [ + {file = "pre_commit-1.21.0-py2.py3-none-any.whl", hash = "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"}, + {file = "pre_commit-1.21.0.tar.gz", hash = "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850"}, +] +py = [ + {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, + {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, +] +pylint = [ + {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, + {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"}, + {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.14.0.tar.gz", hash = "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"}, + {file = "pytest_asyncio-0.14.0-py3-none-any.whl", hash = "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d"}, +] +pyusb = [ + {file = "pyusb-1.1.0.tar.gz", hash = "sha256:d69ed64bff0e2102da11b3f49567256867853b861178689671a163d30865c298"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +regex = [ + {file = "regex-2020.10.28-cp27-cp27m-win32.whl", hash = "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504"}, + {file = "regex-2020.10.28-cp27-cp27m-win_amd64.whl", hash = "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e"}, + {file = "regex-2020.10.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab"}, + {file = "regex-2020.10.28-cp36-cp36m-win32.whl", hash = "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582"}, + {file = "regex-2020.10.28-cp36-cp36m-win_amd64.whl", hash = "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c"}, + {file = "regex-2020.10.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c"}, + {file = "regex-2020.10.28-cp37-cp37m-win32.whl", hash = "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0"}, + {file = "regex-2020.10.28-cp37-cp37m-win_amd64.whl", hash = "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a"}, + {file = "regex-2020.10.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux1_i686.whl", hash = "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf"}, + {file = "regex-2020.10.28-cp38-cp38-win32.whl", hash = "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f"}, + {file = "regex-2020.10.28-cp38-cp38-win_amd64.whl", hash = "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de"}, + {file = "regex-2020.10.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b"}, + {file = "regex-2020.10.28-cp39-cp39-win32.whl", hash = "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0"}, + {file = "regex-2020.10.28-cp39-cp39-win_amd64.whl", hash = "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e"}, + {file = "regex-2020.10.28.tar.gz", hash = "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +smmap = [ + {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, + {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, +] +stevedore = [ + {file = "stevedore-3.2.2-py3-none-any.whl", hash = "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62"}, + {file = "stevedore-3.2.2.tar.gz", hash = "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +virtualenv = [ + {file = "virtualenv-20.1.0-py2.py3-none-any.whl", hash = "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2"}, + {file = "virtualenv-20.1.0.tar.gz", hash = "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380"}, +] +wrapt = [ + {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, +] +zipp = [ + {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, + {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..da8a135 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[tool.poetry] +name = "actuonix_lac" +version = "1.0.0" +description = "Python module for controlling an Actuonix/Firgelli Linear Actuator Control Board" +authors = ["Jerimiah Daley", "Richard Emile Sarkis ", "Eero af Heurlin "] +repository = "https://github.com/DamnedFacts/actuonix-lac" +license = "MIT" +readme = "README.rst" + +[tool.poetry.scripts] +laccontrol = "actuonix_lac.console:laccontrol" + +[tool.poetry.dependencies] +python = "^3.7" +pyusb = "^1.1" +click = { version = "^7.1", optional = true} + +[tool.poetry.extras] +cli = ["click"] +all = ["click"] + +[tool.poetry.dev-dependencies] +pytest = "^6.1" +bump2version = "^0.5.11" +pre-commit = "^1.20" +pylint = "^2.5" +black = "=20.08.b1" +bandit = "^1.6" +mypy = "^0.790" +pytest-asyncio = "^0.14" + + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 120 +target-version = ['py37'] +exclude = ''' +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + )/ + | __pycache__ +) +''' + +[tool.pylint.format] +max-line-length = 120 + +[tool.pylint.messages_control] +disable=["fixme", "W1202"] + +[tool.coverage.run] +omit = ["tests/*"] +branch = true diff --git a/setup.py b/setup.py deleted file mode 100644 index d2219eb..0000000 --- a/setup.py +++ /dev/null @@ -1,11 +0,0 @@ -from setuptools import setup - -setup(name='actuonix_lac', - version='0.1', - description='Python-based controller for Actuonix Linear Actuator Arm (LAC)', - url='https://github.com/DamnedFacts/actuonix-lac', - author='Jerimiah Daley, and Richard Emile Sarkis', - author_email='rich@sark.is', - license='MIT', - packages=['actuonix_lac'], - zip_safe=False) diff --git a/src/actuonix_lac/__init__.py b/src/actuonix_lac/__init__.py new file mode 100644 index 0000000..7651863 --- /dev/null +++ b/src/actuonix_lac/__init__.py @@ -0,0 +1,2 @@ +"""Python module for controlling an Actuonix/Firgelli Linear Actuator Control Board""" +__version__ = "1.0.0" diff --git a/src/actuonix_lac/aio.py b/src/actuonix_lac/aio.py new file mode 100644 index 0000000..dd573dd --- /dev/null +++ b/src/actuonix_lac/aio.py @@ -0,0 +1,133 @@ +"""asyncio implementation""" +from typing import Union +import asyncio + +import usb.core # type: ignore + +from .common import Commands, pyusb_blocking, device_config_init_graceful + + +class AsyncLAC: # pylint: disable=R0904 + """Communicate with the LAC board (blocking version)""" + + _lock: asyncio.Lock + + def __init__(self, vendorID: int = 0x4D8, productID: int = 0xFC5F): + self.device = usb.core.find(idVendor=vendorID, idProduct=productID) # Defaults for our LAC; give yours a test + if self.device is None: + raise RuntimeError("No board found, ensure board is connected and powered and matching the IDs provided") + self._lock = asyncio.Lock() + device_config_init_graceful(self.device) + + async def send_data(self, command: Union[int, Commands], value: int = 0) -> int: + """Take data and send it to LAC""" + if value < 0 or value > 1023: + raise ValueError("Value is OOB. Must be 2-byte integer in rage [0, 1023], was {}".format(value)) + if int(command) not in Commands.values(): + raise ValueError("command is OOB, see the Commands enum for valid values. was {}".format(command)) + + async with self._lock: + return await asyncio.get_event_loop().run_in_executor(None, pyusb_blocking, command, value, self.device) + + async def set_accuracy(self, value: int = 4) -> None: + """How close to target distance is accepted value/1024 * stroke gives distance, where stroke is max + extension length (all values in mm). Round to nearest integer""" + await self.send_data(Commands.SET_ACCURACY, value) + + async def set_retract_limit(self, value: int) -> None: + """How far back the actuator can go. A value of 0 hits the mechanical stop, but this is not recommended. + The value you want to send is calculated by (distance * 1023)/stroke where distance is intended distance + and stroke is max extension length, all values in mm. Round to nearest integer""" + await self.send_data(Commands.SET_RETRACT_LIMIT, value) + + async def set_extend_limit(self, value: int) -> None: + """How far forward the actuator can go. A value of 1023 hits the mechanical stop, + but this is not recommended. See above for math""" + await self.send_data(Commands.SET_EXTEND_LIMIT, value) + + async def set_movement_threshold(self, value: int) -> None: + """Minimum speed before actuator is considered stalling""" + await self.send_data(Commands.SET_MOVEMENT_THRESHOLD, value) + + async def set_stall_time(self, value: int) -> None: + """Timeout (ms) before actuator shuts off after stalling""" + await self.send_data(Commands.SET_STALL_TIME, value) + + async def set_pwm_threshold(self, value: int) -> None: + """When feedback-set>this, set speed to maximum""" + await self.send_data(Commands.SET_PWM_THRESHOLD, value) + + async def set_derivative_threshold(self, value: int) -> None: + """Compared to measured speed to determine PWM increase (prevents stalls). + Normally equal to movement threshold""" + await self.send_data(Commands.SET_DERIVATIVE_THRESHOLD, value) + + async def set_max_derivative(self, value: int) -> None: + """Maximum value the D term can contribute to control speed""" + await self.send_data(Commands.SET_MAX_DERIVATIVE, value) + + async def set_min_derivative(self, value: int) -> None: + """Minimum value the D term can contribute to control speed""" + await self.send_data(Commands.SET_MIN_DERIVATIVE, value) + + async def set_max_pwm_value(self, value: int) -> None: + """Speed the actuator runs at when outside the pwm threshold 1023 enables top speed, + though actuator may try to move faster to avoid stalling""" + await self.send_data(Commands.SET_MAX_PWM_VALUE, value) + + async def set_min_pwm_value(self, value: int) -> None: + """Minimum PWM value applied by PD""" + await self.send_data(Commands.SET_MIN_PWM_VALUE, value) + + async def set_proportional_gain(self, value: int) -> None: + """Higher value = faster approach to target, but also more overshoot""" + await self.send_data(Commands.SET_PROPORTIONAL_GAIN, value) + + async def set_derivative_gain(self, value: int) -> None: + """Rate at which differential portion of controller increases while stalling. Not a /real/ differential term, + but similar effect. When stalling, derivtive term is incremented to attempt escape""" + await self.send_data(Commands.SET_DERIVATIVE_GAIN, value) + + async def set_average_rc(self, value: int = 4) -> None: + """Number of samples used in filtering the RC input signal before the actuator moves. + High value = more stability, but lower response time. value * 20ms = delay time. This does NOT affect filter + feedback delay; control response to valid input signals is unaffected""" + await self.send_data(Commands.SET_AVERAGE_RC, value) + + async def set_average_adc(self, value: int) -> None: + """Number of samples used in filtering the feedback and analog input signals, if active. + Similar delay effect to set_average_rc, but this DOES affect control response. + PD loop values may need to be retuned when adjusting this""" + await self.send_data(Commands.SET_AVERAGE_ADC, value) + + async def get_feedback(self) -> int: + """Causes actuator to send a feedback packet containing its current position. + This is read directly from ADC and might not be equal to the set point if yet unreached""" + return await self.send_data(Commands.GET_FEEDBACK) + + async def set_position(self, value: int) -> None: + """Set the LAC's position. This shouldn't be shocking, given like ya know the name of the function? + Note that this will disable RC, I, and V inputs until reboot. + To know what number to send, do (distance * 1023)/stroke where distance is intended position as a distance + from the back hardstop, in mm, and stroke is the maximum length of extension, in mm. + Be sure to round your result to the nearest integer!""" + await self.send_data(Commands.SET_POSITION, value) + + async def set_speed(self, value: int) -> None: + """This command is not documented, but it's probably easy to infer and just guess via trial by fire""" + await self.send_data(Commands.SET_SPEED, value) + + async def disable_manual(self) -> None: + """Saves current config to EEPROM and disables all four potentiometers. + On reboot, these values will continue being used instead of the potentiometer values. + Analog inputs function as normal either way""" + await self.send_data(Commands.DISABLE_MANUAL) + + async def reset(self) -> None: + """Enables manual control potentiometers and resets config to factory default""" + await self.send_data(Commands.RESET) + + def __del__(self) -> None: + """Release the device""" + if self.device: + usb.util.dispose_resources(self.device) diff --git a/src/actuonix_lac/blocking.py b/src/actuonix_lac/blocking.py new file mode 100644 index 0000000..f422b24 --- /dev/null +++ b/src/actuonix_lac/blocking.py @@ -0,0 +1,133 @@ +"""The old blocking implementation""" +from typing import Union +import threading + +import usb.core # type: ignore +import usb.util # type: ignore + +from .common import Commands, pyusb_blocking, device_config_init_graceful + + +class LAC: # pylint: disable=R0904 + """Communicate with the LAC board (blocking version)""" + + _lock: threading.Lock + + def __init__(self, vendorID: int = 0x4D8, productID: int = 0xFC5F): + self.device = usb.core.find(idVendor=vendorID, idProduct=productID) # Defaults for our LAC; give yours a test + if self.device is None: + raise Exception("No board found, ensure board is connected and powered and matching the IDs provided") + self._lock = threading.Lock() + device_config_init_graceful(self.device) + + def send_data(self, command: Union[int, Commands], value: int = 0) -> int: + """Take data and send it to LAC""" + if value < 0 or value > 1023: + raise ValueError("Value is OOB. Must be 2-byte integer in rage [0, 1023], was {}".format(value)) + if int(command) not in Commands.values(): + raise ValueError("command is OOB, see the Commands enum for valid values. was {}".format(command)) + with self._lock: + return pyusb_blocking(int(command), value, self.device) + + def set_accuracy(self, value: int = 4) -> None: + """How close to target distance is accepted value/1024 * stroke gives distance, where stroke is max + extension length (all values in mm). Round to nearest integer""" + self.send_data(Commands.SET_ACCURACY, value) + + def set_retract_limit(self, value: int) -> None: + """How far back the actuator can go. A value of 0 hits the mechanical stop, but this is not recommended. + The value you want to send is calculated by (distance * 1023)/stroke where distance is intended distance + and stroke is max extension length, all values in mm. Round to nearest integer""" + self.send_data(Commands.SET_RETRACT_LIMIT, value) + + def set_extend_limit(self, value: int) -> None: + """How far forward the actuator can go. A value of 1023 hits the mechanical stop, + but this is not recommended. See above for math""" + self.send_data(Commands.SET_EXTEND_LIMIT, value) + + def set_movement_threshold(self, value: int) -> None: + """Minimum speed before actuator is considered stalling""" + self.send_data(Commands.SET_MOVEMENT_THRESHOLD, value) + + def set_stall_time(self, value: int) -> None: + """Timeout (ms) before actuator shuts off after stalling""" + self.send_data(Commands.SET_STALL_TIME, value) + + def set_pwm_threshold(self, value: int) -> None: + """When feedback-set>this, set speed to maximum""" + self.send_data(Commands.SET_PWM_THRESHOLD, value) + + def set_derivative_threshold(self, value: int) -> None: + """Compared to measured speed to determine PWM increase (prevents stalls). + Normally equal to movement threshold""" + self.send_data(Commands.SET_DERIVATIVE_THRESHOLD, value) + + def set_max_derivative(self, value: int) -> None: + """Maximum value the D term can contribute to control speed""" + self.send_data(Commands.SET_MAX_DERIVATIVE, value) + + def set_min_derivative(self, value: int) -> None: + """Minimum value the D term can contribute to control speed""" + self.send_data(Commands.SET_MIN_DERIVATIVE, value) + + def set_max_pwm_value(self, value: int) -> None: + """Speed the actuator runs at when outside the pwm threshold 1023 enables top speed, + though actuator may try to move faster to avoid stalling""" + self.send_data(Commands.SET_MAX_PWM_VALUE, value) + + def set_min_pwm_value(self, value: int) -> None: + """Minimum PWM value applied by PD""" + self.send_data(Commands.SET_MIN_PWM_VALUE, value) + + def set_proportional_gain(self, value: int) -> None: + """Higher value = faster approach to target, but also more overshoot""" + self.send_data(Commands.SET_PROPORTIONAL_GAIN, value) + + def set_derivative_gain(self, value: int) -> None: + """Rate at which differential portion of controller increases while stalling. Not a /real/ differential term, + but similar effect. When stalling, derivtive term is incremented to attempt escape""" + self.send_data(Commands.SET_DERIVATIVE_GAIN, value) + + def set_average_rc(self, value: int = 4) -> None: + """Number of samples used in filtering the RC input signal before the actuator moves. + High value = more stability, but lower response time. value * 20ms = delay time. This does NOT affect filter + feedback delay; control response to valid input signals is unaffected""" + self.send_data(Commands.SET_AVERAGE_RC, value) + + def set_average_adc(self, value: int) -> None: + """Number of samples used in filtering the feedback and analog input signals, if active. + Similar delay effect to set_average_rc, but this DOES affect control response. + PD loop values may need to be retuned when adjusting this""" + self.send_data(Commands.SET_AVERAGE_ADC, value) + + def get_feedback(self) -> int: + """Causes actuator to send a feedback packet containing its current position. + This is read directly from ADC and might not be equal to the set point if yet unreached""" + return self.send_data(Commands.GET_FEEDBACK) + + def set_position(self, value: int) -> None: + """Set the LAC's position. This shouldn't be shocking, given like ya know the name of the function? + Note that this will disable RC, I, and V inputs until reboot. + To know what number to send, do (distance * 1023)/stroke where distance is intended position as a distance + from the back hardstop, in mm, and stroke is the maximum length of extension, in mm. + Be sure to round your result to the nearest integer!""" + self.send_data(Commands.SET_POSITION, value) + + def set_speed(self, value: int) -> None: + """This command is not documented, but it's probably easy to infer and just guess via trial by fire""" + self.send_data(Commands.SET_SPEED, value) + + def disable_manual(self) -> None: + """Saves current config to EEPROM and disables all four potentiometers. + On reboot, these values will continue being used instead of the potentiometer values. + Analog inputs function as normal either way""" + self.send_data(Commands.DISABLE_MANUAL) + + def reset(self) -> None: + """Enables manual control potentiometers and resets config to factory default""" + self.send_data(Commands.RESET) + + def __del__(self) -> None: + """Release the device""" + if self.device: + usb.util.dispose_resources(self.device) diff --git a/src/actuonix_lac/common.py b/src/actuonix_lac/common.py new file mode 100644 index 0000000..611e4f8 --- /dev/null +++ b/src/actuonix_lac/common.py @@ -0,0 +1,63 @@ +"""Common enums etc""" +from typing import Iterable, Any +import enum +import time +import struct + +import usb.core # type: ignore + + +class Commands(enum.IntEnum): + """The commands""" + + SET_ACCURACY = 0x01 + SET_RETRACT_LIMIT = 0x02 + SET_EXTEND_LIMIT = 0x03 + SET_MOVEMENT_THRESHOLD = 0x04 + SET_STALL_TIME = 0x05 + SET_PWM_THRESHOLD = 0x06 + SET_DERIVATIVE_THRESHOLD = 0x07 + SET_MAX_DERIVATIVE = 0x08 + SET_MIN_DERIVATIVE = 0x09 + SET_MAX_PWM_VALUE = 0x0A + SET_MIN_PWM_VALUE = 0x0B + SET_PROPORTIONAL_GAIN = 0x0C + SET_DERIVATIVE_GAIN = 0x0D + SET_AVERAGE_RC = 0x0E + SET_AVERAGE_ADC = 0x0F + GET_FEEDBACK = 0x10 + SET_POSITION = 0x20 + SET_SPEED = 0x21 + DISABLE_MANUAL = 0x30 + RESET = 0xFF + + @classmethod + def names(cls) -> Iterable[str]: + """List all command names""" + return (reg.name for reg in cls) + + @classmethod + def values(cls) -> Iterable[int]: + """List all command int values""" + return (reg.value for reg in cls) + + +def pyusb_blocking(command: int, value: int, device: Any) -> int: + """Handle the blocking pyusb send and read""" + data = struct.pack( + b"BBB", int(command), value & 0xFF, (value & 0xFF00) >> 8 + ) # Low byte masked in, high byte masked and moved down + device.write(1, data, 100) # Magic numbers from the PyUSB tutorial + time.sleep(0.05) # Just to be sure it's all well and sent + response = device.read(0x81, 3, 100) # 3 because there's three bytes to a packet + return int((response[2] << 8) + response[1]) # High byte moved left, then tack on the low byte + + +def device_config_init_graceful(device: Any) -> None: + """Make sure configuration is selected but do not reset selected config""" + try: + cfg = device.get_active_configuration() + except usb.core.USBError: + cfg = None + if cfg is None: + device.set_configuration() diff --git a/src/actuonix_lac/console.py b/src/actuonix_lac/console.py new file mode 100644 index 0000000..fc6edbb --- /dev/null +++ b/src/actuonix_lac/console.py @@ -0,0 +1,48 @@ +"""CLI entrypoints""" +from typing import Any +import asyncio +import logging + +import click +from actuonix_lac.aio import AsyncLAC + + +LOGGER = logging.getLogger(__name__) + + +@click.group() +@click.option("-l", "--loglevel", help="Python log level, 10=DEBUG, 20=INFO, 30=WARNING, 40=CRITICAL", default=30) +@click.option("-v", "--verbose", count=True, help="Shorthand for info/debug loglevel (-v/-vv)") +@click.pass_context +def commonopts(ctx: Any, loglevel: int, verbose: int) -> None: + """CLI wrapper for select methods in AsyncLAC""" + if verbose == 1: + loglevel = 20 + if verbose >= 2: + loglevel = 10 + logging.getLogger("").setLevel(loglevel) + LOGGER.setLevel(loglevel) + ctx.ensure_object(dict) + ctx.obj["servo"] = AsyncLAC() + ctx.obj["loop"] = asyncio.get_event_loop() + + +@commonopts.command() +@click.argument("value", type=int) +@click.pass_context +def set_position(ctx: Any, value: int) -> None: + """Set the raw position, between 0-1023""" + ctx.obj["loop"].run_until_complete(ctx.obj["servo"].set_position(value)) + + +@commonopts.command() +@click.pass_context +def get_position(ctx: Any) -> None: + """Get the raw actual position, between 0-1023""" + pos = ctx.obj["loop"].run_until_complete(ctx.obj["servo"].get_feedback()) + click.echo(pos) + + +def laccontrol() -> None: + """CLI entrypoint""" + commonopts() # pylint: disable=E1120 diff --git a/src/actuonix_lac/lac.py b/src/actuonix_lac/lac.py new file mode 100644 index 0000000..a399033 --- /dev/null +++ b/src/actuonix_lac/lac.py @@ -0,0 +1,2 @@ +"""Backwards compatibility, import the blocking version""" +from .blocking import LAC # pylint: disable=W0611 diff --git a/actuonix_lac/__init__.py b/src/actuonix_lac/py.typed similarity index 100% rename from actuonix_lac/__init__.py rename to src/actuonix_lac/py.typed diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..dccecf4 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +"""Tests for actuonix_lac""" +# TODO: How to mock USB for unit testing ? diff --git a/tests/test_actuonix_lac.py b/tests/test_actuonix_lac.py new file mode 100644 index 0000000..8ab442a --- /dev/null +++ b/tests/test_actuonix_lac.py @@ -0,0 +1,7 @@ +"""Module level tests""" +from actuonix_lac import __version__ + + +def test_version() -> None: + """Make sure version string is correct""" + assert __version__ == "1.0.0" diff --git a/tests/test_realhw.py b/tests/test_realhw.py new file mode 100644 index 0000000..d0bc9e8 --- /dev/null +++ b/tests/test_realhw.py @@ -0,0 +1,49 @@ +"""Test with real hardware""" +import time +import os +import asyncio + +import pytest + +from actuonix_lac.blocking import LAC +from actuonix_lac.aio import AsyncLAC + +pytestmark = pytest.mark.skipif(not os.environ.get("LACHWTEST"), reason="Must LACHWTEST to test HW") + + +@pytest.mark.parametrize("expected_position", [16, 256, 32]) +def test_blocking_position(expected_position: int) -> None: + """Test blocking version""" + servo = LAC() + position_grace = 4 + servo.set_accuracy(position_grace) + servo.set_position(expected_position) + started = time.time() + while True: + if time.time() - started > 10: + raise TimeoutError("Timed out") + true_position = servo.get_feedback() + if (expected_position - position_grace) < true_position < (expected_position + position_grace): + break + time.sleep(0.1) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("expected_position", [16, 256, 32]) +async def test_async_position(expected_position: int) -> None: + """Test blocking version""" + servo = AsyncLAC() + position_grace = 4 + await servo.set_accuracy(position_grace) + await servo.set_position(expected_position) + + async def wait_for_position() -> None: + """Loop until position is what we want""" + nonlocal expected_position, position_grace + while True: + true_position = await servo.get_feedback() + if (expected_position - position_grace) < true_position < (expected_position + position_grace): + break + await asyncio.sleep(0.5) + + await asyncio.wait_for(wait_for_position(), timeout=10)