diff --git a/.github/workflows/FlakeHell.yml b/.github/workflows/FlakeHell.yml new file mode 100644 index 0000000..dfd70a8 --- /dev/null +++ b/.github/workflows/FlakeHell.yml @@ -0,0 +1,32 @@ +# FlakeHell is a flake8 wrapper which adds features like having a baseline +# https://github.com/flakehell/flakehell +name: FlakeHell + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Poetry + uses: abatilo/actions-poetry@v2.0.0 + with: + poetry-version: 1.1.11 + - name: Install dependencies + run: poetry install + - name: Run FlakeHell + run: poetry run flakehell lint diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index a4f521c..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,27 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: CI - -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch -on: - push: - branches: ['*'] - pull_request: - branches: [ master ] - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - - name: GitHub Action for pylint - uses: cclauss/GitHub-Action-for-pylint@0.7.0 - diff --git a/baseline.txt b/baseline.txt new file mode 100644 index 0000000..e61d088 --- /dev/null +++ b/baseline.txt @@ -0,0 +1,118 @@ +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> update baseline.txt to have lastest errors +4a60c5d308255d0d2925ad3e5b6af35a +47badeecef7e5f42d0fec8b23b6d5b16 +0b49d4ddd0c4a781c29df26ec0705e2d +7e1d0671a7ba98c19afd89bac18b74d6 +cf23ad94be9a5bf46a1d37a2ca010ddd +363e58b9182139e99f7726868ef7370b +99f707790a1bf61a355ded6dd4c3ac94 +01cb4f52fd6926dad26cc76c0d84aad6 +3d35e9e92a171de0e3e7e2f83ccd7946 +3d35e9e92a171de0e3e7e2f83ccd7946 +3d35e9e92a171de0e3e7e2f83ccd7946 +1219f8f56bc1a5c622e9399625a3d90a +f6d05f5271ff275f43ae348bda03c43c +7406d784622e7c96838542366d0db482 +735ccdb6af07c3d02bb6c944c060d546 +<<<<<<< HEAD +======= +>>>>>>> Add support for FlakeHell and run in CI +======= +>>>>>>> update baseline.txt to have lastest errors +0d06fc0fcbc5d689a8d141bc6d4d3f54 +1d52ca85056e3e6880ccc3ae7b245198 +8159c937f68f3ef4ea6ba4e8e2e06e9c +8159c937f68f3ef4ea6ba4e8e2e06e9c +f2bee477886f477c377fc242a258216f +f2bee477886f477c377fc242a258216f +ac6d400ffa3469a99d16227c3b15c13d +574d9d08adf774fba1eb8a08b405081d +67f8d9ded827b03fa1b4063fedfc3274 +873a39057418f171abae43c29c29e04b +668b5acfa0c72fa05cc66729c08dad01 +5ef54202da37c52f9d3ac59382164c13 +d81a44ed2cc712f4ec1a6310ca35bc2c +4f03d466ca6a9c6a4c8ff4154ff72540 +3575fabb6bd69a51f4fed0f99842db86 +26d98d6b85a5abd6e0771217a2fab6b0 +fa9a834b291d8e4a00567a23382b35d2 +9105bc37b6b5b4b2a835f524d9b7dd1c +cf3d2831fdd58b1f94ea7f5cbd54faaf +273f1c5e5b319ce80401c29d2e70fd64 +e37b4b9be251f3a494f390550fcb24d1 +48ff7d155dcfa6ff81a309131f4e4ce9 +951cf668dcefccd7f75cb76461f451b1 +a446e67fc7d4109a6558e64c9ca9c979 +a3d129cf8030180c825f0f3702578aa7 +293e19620ef6b8fa5306cd3a1d606ee1 +77f894c59b918e0b6aac1376ff6bf546 +0245f23825d734dcc312e5a9737b374e +ae2710b46bdf7c623eee03c076023489 +882bf529f917a2836c3711f664ce28a9 +b5b7cab2934fc4135529ef9917902e94 +0758713749e58850ff007d0f72d6c788 +8c01248c55e383fa59a8d1a20bc6eadb +9face4f55d9dfcc9b1e405ecdd017ec2 +a2cdbf5f77f92d563e44d821c2789245 +c127980a22faf700223c67265d741672 +79d2f917d5189ce0ea6836a936d08fdc +c245945d5ac1b2353371e3de2193cc8b +6998ba7775821d4a02e70d23939dfdba +15fe2c56b1c2b697c9c57214f06dccdc +cd96450e0fdd988ae4e61f13484a0c8f +353bc9f12f156bcaff167de7dd98a579 +7b61e3d35bb06f76e95a450597c46f40 +3e215cf30f2669b085c4c9936381c28d +a6a35b5a9a404cdfb4a96d508cdfe44b +08c20027906fd713ed9fe99c1d7f1268 +7f5ab474fd397912907ac39d25a94c00 +3bb257d1a4492982010d4798b882211f +a0278b1ce038751b93b56937e7974dc4 +a0278b1ce038751b93b56937e7974dc4 +a408f8d4ff04d329cabda519d4536197 +ea6d3b993dfe4b27e451743f1d0d5a2f +ea6d3b993dfe4b27e451743f1d0d5a2f +ea6d3b993dfe4b27e451743f1d0d5a2f +b8d2a217155ebf6448aea393bb028de2 +1969cfbcb6870aaaeeef4b4f1c98d8a7 +07dbd77361d45271ed2dac28fa0b2685 +7ee6a2a75860c76a9dafe2e9bb889566 +7ee6a2a75860c76a9dafe2e9bb889566 +7ee6a2a75860c76a9dafe2e9bb889566 +7ee6a2a75860c76a9dafe2e9bb889566 +7ee6a2a75860c76a9dafe2e9bb889566 +7ee6a2a75860c76a9dafe2e9bb889566 +7ee6a2a75860c76a9dafe2e9bb889566 +7ee6a2a75860c76a9dafe2e9bb889566 +4318656d10019fc78b4caaccdfca139c +4318656d10019fc78b4caaccdfca139c +e753abcc9e8418ca5cb4b15d2e4e6fae +0843af79d1bfa0848262a9920ef5a8ef +508bbf5f9985629c8a75c7b240f06cfd +91480136f746bcec643d77978fce44b2 +13b6998f6a9916f6537199b64fcc97bb +13b6998f6a9916f6537199b64fcc97bb +4e63655e1fbfda620ab773cf8576a529 +47fe3bc244a8b58883b75f09b8ca1cdc +fa1063f014f1691166732b213da92a42 +7ac2a27aa3690652176c93230c1b96f8 +bcb157db4e23e1f4307e36f377e33e77 +f148cab974011c70881fa463d95b56e4 +a91a8f69e1c31c67b20c2de5d201e5f4 +3cca2f9f923efd1e04b17b5c49cd4c07 +cd5b61cab3b1bd898877c0f802a9b9a4 +7ab39fc6af00f768ff77163a6496ee90 +557403f8ab82a053ea4feaa53be80808 +557403f8ab82a053ea4feaa53be80808 +557403f8ab82a053ea4feaa53be80808 +dd6633910df3928581921571ac30127b +dd6633910df3928581921571ac30127b +dd6633910df3928581921571ac30127b +6922db4e43d4a8263d6e49b6943a55ba +3366b6f746d65cbf36e8b2041fbf8383 +4c2f971d1a2a5c4bd3b8371955c8d428 +b16d0d3e6b42cdb55c560948380cf2a7 +b16d0d3e6b42cdb55c560948380cf2a7 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c3084bd --- /dev/null +++ b/poetry.lock @@ -0,0 +1,199 @@ +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "entrypoints" +version = "0.3" +description = "Discover and load entry points from installed packages." +category = "main" +optional = false +python-versions = ">=2.7" + +[[package]] +name = "flake8" +version = "3.8.0" +description = "the modular source code checker: pep8 pyflakes and co" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[[package]] +name = "flakehell" +version = "0.9.0" +description = "Flake8 wrapper to make it nice and configurable" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = "*" +entrypoints = "*" +flake8 = ">=3.8.0" +pygments = "*" +toml = "*" +urllib3 = "*" + +[package.extras] +docs = ["alabaster", "pygments-github-lexers", "recommonmark", "sphinx"] +dev = ["dlint", "flake8-2020", "flake8-aaa", "flake8-absolute-import", "flake8-alfred", "flake8-annotations-complexity", "flake8-bandit", "flake8-black", "flake8-broken-line", "flake8-bugbear", "flake8-builtins", "flake8-coding", "flake8-cognitive-complexity", "flake8-commas", "flake8-comprehensions", "flake8-debugger", "flake8-django", "flake8-docstrings", "flake8-eradicate", "flake8-executable", "flake8-expression-complexity", "flake8-fixme", "flake8-functions", "flake8-future-import", "flake8-import-order", "flake8-isort", "flake8-logging-format", "flake8-mock", "flake8-mutable", "flake8-mypy", "flake8-pep3101", "flake8-pie", "flake8-print", "flake8-printf-formatting", "flake8-pyi", "flake8-pytest", "flake8-pytest-style", "flake8-quotes", "flake8-requirements", "flake8-rst-docstrings", "flake8-scrapy", "flake8-spellcheck", "flake8-sql", "flake8-strict", "flake8-string-format", "flake8-tidy-imports", "flake8-todo", "flake8-use-fstring", "flake8-variables-names", "isort", "mccabe", "pandas-vet", "pep8-naming", "pylint", "pytest", "typing-extensions", "wemake-python-styleguide"] + +[[package]] +name = "importlib-metadata" +version = "4.8.2" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.10.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "b016738e7fd492f65198c91b774d98551a39d3127ced5794d8104a4f9f4bc2a0" + +[metadata.files] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +entrypoints = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] +flake8 = [ + {file = "flake8-3.8.0-py2.py3-none-any.whl", hash = "sha256:bcf5163890bb01f11f04f0f444f01004d0f9ad5fab10c51104f770acf532008f"}, + {file = "flake8-3.8.0.tar.gz", hash = "sha256:e2f33066fb92ac0a3a30ea509f61e325f2110b2e84644333a3ff8e9e98a2beab"}, +] +flakehell = [ + {file = "flakehell-0.9.0-py3-none-any.whl", hash = "sha256:48a3a9b46136240e52b3b32a78a0826c45f6dcf7d980c30f758c1db5b1439c0b"}, + {file = "flakehell-0.9.0.tar.gz", hash = "sha256:208836d8d24194d50cfa4c1fc99f681f3c537cc232edcd06455abc2971460893"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, + {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pygments = [ + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, +] +urllib3 = [ + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] +zipp = [ + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6d2d54a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "robot" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.7" +flake8 = "3.8.0" +flakehell = "^0.9.0" + +[tool.poetry.dev-dependencies] + +[tool.flakehell] +# Inherit config +base = "https://raw.githubusercontent.com/life4/flakehell/master/pyproject.toml" +format = "grouped" +max_line_length = 100 +show_source = true +baseline = "baseline.txt" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/robot/calibrate_camera.py b/robot/calibrate_camera.py deleted file mode 100755 index 2d8c31c..0000000 --- a/robot/calibrate_camera.py +++ /dev/null @@ -1,71 +0,0 @@ -"""A script for getting the focal length luts for a camera -It losely follows the ideas of a PD controller combinded with a NM gradient -descent algo. -Usage: -import robot.calibrate_camera -""" -import robot -import math -import pprint - -TARGET = 3.0 -THRESHOLD = 0.01 -KP = 100 -KD = 5 -K_READING_COUNTS = 0.5 - - -def _get_reading(): - while True: - try: - return R.see()[0].dist - except IndexError: - print("saw nothing") - - -def _get_reading_number(error): - result = int(K_READING_COUNTS / error) - result = abs(result) - if result is 0: - result = 1 - elif result > 6: - result = 6 - return result - - -R = robot.Robot() -result = {} - -for res in R.camera.focal_lengths.copy(): - print("Checking res {}".format(res)) - R.camera.res = res - pprint.pprint(R.camera.focal_lengths) - - error = THRESHOLD + 1.0 - previous_error = error - while abs(error) > THRESHOLD: - value = R.camera.focal_lengths[res][0] - p = error * KP - d = (previous_error - error) * KD - value += p + d - - R.camera.focal_lengths[res] = (value, value) - R.camera._update_camera_params() - - reading_counts = get_reading_number(error) - - dists = [get_reading() for _ in range(reading_counts)] - average_dist = (sum(dists))/reading_counts - - previous_error = error - error = TARGET - average_dist - - print("Tried: {} got dist {} error: {}".format( - value, average_dist, error)) - print(" Max: {} min: {} range: {}".format( - max(dists), min(dists), max(dists) - min(dists))) - print(" P = {} reading_counts {}".format(error * KP, reading_counts)) - - result[res] = (value, value) - -pprint.pprint(result) diff --git a/robot/greengiant.py b/robot/greengiant.py index a7133a5..85615cc 100644 --- a/robot/greengiant.py +++ b/robot/greengiant.py @@ -104,7 +104,7 @@ def read_high_low_data(bus, high_addr, low_addr): return low_value + (high_value << 8) -class GreenGiantInternal(object): +class GreenGiantInternal(): """Intertions for use with the user not intended to be part of the robot API""" @@ -139,10 +139,11 @@ def get_fvr_reading(self): class GreenGiantGPIOPin(): - def __init__(self, bus, index, adc_max): + def __init__(self, pin_list, bus, index, adc_max): + self._pin_list = pin_list self._bus = bus self._index = index - self._mode = None + self._mode = INPUT self._adc_max = adc_max self._digital_read_modes = (INPUT, INPUT_PULLUP, OUTPUT) self._analog_read_modes = (INPUT_ANALOG, INPUT_PULLUP) @@ -153,10 +154,20 @@ def mode(self): @mode.setter def mode(self, mode): + """Update the mode of all of the pins. + Due to a bug in the GG software all of the pins modes need to be updated + in order + `_pin_list.update_mode` triggers all of the pins to be re-updated with + the new settings + """ self._mode = mode - mask = _GG_GPIO_MASKS[mode] - self._bus.write_byte_data( - _GG_I2C_ADDR, _GG_CONTROL_START + self._index, mask) + self._pin_list.update_modes() + + def update_mode(self): + """Writes a mode update for this pin only to the I2C bus""" + mask = _GG_GPIO_MASKS[self._mode] + self._bus.write_byte_data(_GG_I2C_ADDR, _GG_CONTROL_START + self._index, mask) + @property def digital(self): @@ -206,8 +217,11 @@ class GreenGiantGPIOPinList(): """A list of pins indexed from 1""" def __init__(self, bus, adc_max): - self._list = [GreenGiantGPIOPin(bus, i, adc_max) + self._list = [GreenGiantGPIOPin(self, bus, i, adc_max) for i in range(4)] + self.update_modes() # Make sure the state of the GG matches the state + # which we have assumed (INPUT) when creating + # GreenGiantGPIOPin's def __getitem__(self, index): return self._list[_decrement_pin_index(index)] @@ -216,6 +230,14 @@ def __setitem__(self, index, value): internal_index = _decrement_pin_index(index) self._list[internal_index] = value + def update_modes(self): + """All of the modes must be updated in order due to a bug in the GG + This function provides a function aware of the state of all of the pins + which can update them on the GG. + """ + for pin in self._list: + pin.update_mode() + class GreenGiantPWM(): """An object implementing a descriptor protocol to control the servos""" diff --git a/robot/reset.py b/robot/reset.py index 75227a2..84ce17e 100644 --- a/robot/reset.py +++ b/robot/reset.py @@ -22,8 +22,9 @@ def reset(): c.CytronBoard(1).stop() gg.GreenGiantPWM(bus).off() - for i in range(4): - gg.GreenGiantGPIOPin(bus, i, 4.096).mode = gg.INPUT + gpios = gg.GreenGiantGPIOPinList(bus, 4.096) + for i in range(1,5): + gpios[i].mode = gg.INPUT internal = gg.GreenGiantInternal(bus) internal.set_12v(False) internal.set_status_led(True) diff --git a/robot/vision.py b/robot/vision.py index ed8a0f4..239ac93 100644 --- a/robot/vision.py +++ b/robot/vision.py @@ -351,6 +351,10 @@ def __init__(self, bounding_box=True, usb_stick=False, send_to_sheep=False, +<<<<<<< HEAD +======= + +>>>>>>> mend save=True): super(PostProcessor, self).__init__() @@ -401,11 +405,19 @@ def _draw_bounding_box(self, frame, detections): colour, thickness=self._bounding_box_thickness*3) else: +<<<<<<< HEAD cv2.polylines(frame, [integer_corners], polygon_is_closed, colour, thickness=self._bounding_box_thickness) +======= + cv2.polylines(frame, + [integer_corners], + polygon_is_closed, + colour, + thickness=self._bounding_box_thickness) +>>>>>>> mend return frame diff --git a/test/arc.png b/test/arc.png new file mode 100644 index 0000000..d904896 Binary files /dev/null and b/test/arc.png differ diff --git a/test/mosaic.png b/test/mosaic.png new file mode 100644 index 0000000..e6eb61d Binary files /dev/null and b/test/mosaic.png differ diff --git a/test/test_interface.py b/test/test_interface.py new file mode 100644 index 0000000..8d53f13 --- /dev/null +++ b/test/test_interface.py @@ -0,0 +1,140 @@ +"""Test the robot module +For speed all this does it checks that the software interface is consistent and +doesn't raise runtime exceptions. All interactions with the hardware need to +have manual tests +""" +import unittest + +import robot + + +MOTOR_TESTS = [ + (test_value * sign, expected_value * sign, motor) + for motor in (1, 2) + for sign in (1, -1) + for (test_value, expected_value) in ( + (100, 100), # Test max + (100.0, 100), # Test max float + (125, 100), # Test above max + (72, 72), + (15, 15), + (32.342, 32.342), + (0, 0), +)] + + +class RobotInterface(unittest.TestCase): + """The interface behaves as expected; software only""" + + @classmethod + def setUpClass(cls): + cls.r = robot.Robot() + + @classmethod + def tearDownClass(cls): + del cls.r + + def test_see(self): + """R.see() returns a list""" + markers = self.r.see() + self.assertIsInstance(markers, list) + + def test_set_res(self): + """We can set the res #TODO reference a capture to make sure that the res was set right""" + test_res = (10, 10) + + starting_res = self.r.res + self.r.res = test_res + self.assertEqual(self.r.res, test_res) + self.r.res = starting_res + + def test_set_motors(self): + """Motors can be set to a value and can be queried for that value""" + for value, expected, motor in MOTOR_TESTS: + with self.subTest(value=value, expected=expected, motor=motor): + self.r.motors[motor] = value + self.assertEqual(self.r.motors[motor], expected) + self.r.motors[motor] = 0 # Reset the motor state + + @unittest.expectedFailure + def test_servos(self): + """Servos can be set to a value and can be queried for that value""" + servo_tests = [ + (test_value * sign, expected_value, servo) + for servo in (1,2,3,4) + for sign in (1, -1) + for (test_value, expected_value) in ( + (50, 50), # Test integer + (34.23423, 34.23423), # Test float + (110, 110), + (1000, 1000), + (0, 0) + ) + ] + for value, expected, servo in servo_tests: + with self.subTest(value=value, expected=expected, servo=servo): + self.r.servos[servo] = value + self.assertEqual(self.r.servos[servo], expected) + self.r.servos[servo] = 0 # Reset the motor state + + @unittest.expectedFailure + def test_gpio_output_set_and_read(self): + """GPIOs out digit'als can be set and read""" + # TODO This isn't' python3 compatiable (probally need to unit test everything) + gpio_digital_out_tests = [ + (state, pin) + for state in (True, False) + for pin in range(0, 4) + ] + for state, pin in gpio_digital_out_tests: + with self.subTest(state=state, pin=pin): + print(type(self.r.gpio[pin])) + self.r.gpio[pin].mode = robot.OUTPUT + self.r.gpio[pin].digital = state + self.assertEqual(self.r.gpio[pin].digital, state) + + +class RobotInit(unittest.TestCase): + """No runtime errors from the `Robot` `kwargs`""" + def test_motor_max_in_range(self): + """Robot can have a custom max_motor_voltage less than 12V + TODO remove duplicated code from the motor interface test + """ + R = robot.Robot(max_motor_voltage=6) + for value, expected, motor in MOTOR_TESTS: + with self.subTest(value=value, expected=expected, motor=motor): + R.motors[motor] = value + self.assertEqual(R.motors[motor], expected) + R.motors[motor] = 0 # Reset the motor state + + def test_motor_max_float(self): + """Robot can have a custom max_motor_voltage less than 12V and is float + """ + R = robot.Robot(max_motor_voltage=7.2) + for value, expected, motor in MOTOR_TESTS: + with self.subTest(value=value, expected=expected, motor=motor): + R.motors[motor] = value + self.assertEqual(R.motors[motor], expected) + R.motors[motor] = 0 # Reset the motor state + + def test_motor_max_out_range(self): + """Robot max_motor_voltage above 12 prevents init""" + with self.assertRaises(ValueError): + R = robot.Robot(max_motor_voltage=20) + + def test_use_usb_cam(self): + """Robot can be init with a USB camera + TODO raise an error if a USB camera is not plugged in + """ + R = robot.Robot(camera=robot.RoboConUSBCamera) + R.see() + + def test_wait_for_start_pauses_initalisation(self): + """Can move servos before competition starts""" + R = robot.Robot(wait_for_start=False) + R.servos[1] = 100 + R.motors[1] = 100 + R.wait_start() + # Reset state + R.servos[1] = 0 + R.motors[1] = 0 diff --git a/test/test_vision.py b/test/test_vision.py new file mode 100644 index 0000000..814586b --- /dev/null +++ b/test/test_vision.py @@ -0,0 +1,133 @@ +import unittest +import time +import numpy +from datetime import datetime + +import cv2 + +import robot + +image_mosaic = "/home/pi/robot/test/mosaic.png" +image_arc = "/home/pi/robot/test/arc.png" + +mock_fl = (607.6669874845361, 607.6669874845361) + + +class MockCamera(robot.vision.Camera): + """A Mock camera for returning precaptured images""" + def __init__(self): + self._res = (640, 480) + self._mock_fl_lut = { + self.res: mock_fl + } + self._update_camera_params(self._mock_fl_lut) + self.colour_frame = cv2.imread(image_mosaic) + + def set_mock_image(self, path, scale=None): + """Load image located at `path` as the mock image + + scale - the scale factor to scale the image by + """ + self.colour_frame = cv2.imread(path) + height, width, depth = numpy.shape(self.colour_frame) + self._res = (height, width) + if scale is not None: + new_size = (height * scale, width * scale) + cv2.resize(self.colour_frame, new_size, interpolation=cv2.INTER_AREA) + self._res = new_size + self._mock_fl_lut[self._res] = mock_fl + self._update_camera_params(self._mock_fl_lut) + + + @property + def res(self): + return self._res + + @res.setter + def res(self, new_res): + self._res = new_res + + def capture(self): + capture_time = datetime.now() + grey_frame = cv2.cvtColor(self.colour_frame, cv2.COLOR_BGR2GRAY) + return robot.vision.Capture(grey_frame=grey_frame, + colour_frame=self.colour_frame, + colour_type="RGB", + time=capture_time) + + def close(self): + print("MockCamera close called") + + +class RobotVision(unittest.TestCase): + """Tests the vision module behaves as we expect""" + @classmethod + def setUpClass(cls): + cls.r = robot.Robot(camera=MockCamera) + + @classmethod + def tearDownClass(cls): + if cls.r is not None: + del cls.r + cls.r = None + + def test_marker_numbering(self): + """Check the boundaries of the marker numbering is what we expect + + Should help prevent off by one errors. + """ + self.r.camera.set_mock_image(image_mosaic, scale=10) + + # (marker_code, marker_type) + marker_numbering_tests = [ + (0, robot.MARKER_ARENA), + (32, robot.MARKER_ARENA), + (33, robot.MARKER_TOKEN), + (40, robot.MARKER_TOKEN), + (41, robot.MARKER_DEFAULT) + ] + + markers = self.r.see() + print(f"markers {markers}") + # marker_codes = [m.code for m in markers] + # print(f"got {marker_codes}") + # for marker in markers: + # with self.subTest(marker=marker): + # expected_type = None + # for code, type_ in marker_numbering_tests: + # print(f"test_code {code}") + # print(f"marker.code got code {marker.code}") + # if code == marker.code: + # expected_type = type_ + # break + # self.assertEqual(marker.type, expected_type) + + def test_bearings_from_real_image(self): + """test `marker.bear.y` increases left to right in real image + + These numbers were manually checked. + """ + self.r.camera.set_mock_image(image_arc) + + markers = self.r.see() + + tests = [ + (bear_y, rot_y, marker_code) + for (marker_code, bear_y, rot_y) in ( + (22, 5.9020841573582175, 6.588315444505456), + (24, 12.037433933440928, 23.642168800862297), + (58, 18.891952258734502, 53.18260687019854), + (85, -13.968911862136904, -44.657545026570745), + (144, -2.592376356827769, -27.045341210472248), + (198, 26.18739331265824, 60.27103778528233), + + + ) + ] + for expected_bear_y, expected_rot_y, marker_code in tests: + with self.subTest(expected_bear_y=expected_bear_y, + expected_rot_y=expected_rot_y, + marker_code=marker_code): + for m in [m for m in markers if m.code == marker_code]: + self.assertAlmostEqual(m.bear.y, expected_bear_y, delta=0.5) + self.assertAlmostEqual(m.rot.y, expected_rot_y, delta=0.5)