├── .flake8 ├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .tool-versions ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── VERSION ├── buildhat ├── __init__.py ├── color.py ├── colordistance.py ├── data │ ├── firmware.bin │ ├── signature.bin │ └── version ├── devices.py ├── distance.py ├── exc.py ├── force.py ├── hat.py ├── light.py ├── matrix.py ├── motors.py ├── serinterface.py └── wedo.py ├── docs ├── Makefile ├── README.md ├── buildhat │ ├── color.py │ ├── colordistance.py │ ├── colordistancesensor.rst │ ├── colorsensor.rst │ ├── distance.py │ ├── distancesensor.rst │ ├── force.py │ ├── forcesensor.rst │ ├── hat.py │ ├── hat.rst │ ├── images │ │ ├── 45602_prod_TECHNIC_Large_Angular_Motor_01_200.png │ │ ├── 45603_prod_TECHNIC_Medium_Angular_Motor_01_200.png │ │ ├── 45604_prod_TECHNIC_Distance_Sensor_01_200.png │ │ ├── 45605_prod_TECHNIC_Color_Sensor_01_200.png │ │ ├── 45606_prod_TECHNIC_Force_Sensor_01_200.png │ │ ├── 45607_Prod_01_200.png │ │ ├── 45608_Prod_01_200.png │ │ ├── 88008_01_200.jpeg │ │ ├── 88013_motor_l_02_200.jpg │ │ ├── 88014_motor_xl_03_200.jpg │ │ ├── BuildHAT_closeup.jpg │ │ ├── lights.jpg │ │ ├── lpf2.jpg │ │ └── train_motor.jpg │ ├── index.rst │ ├── light.py │ ├── light.rst │ ├── matrix.py │ ├── matrix.rst │ ├── motion.py │ ├── motionsensor.rst │ ├── motor.py │ ├── motor.rst │ ├── motorpair.py │ ├── motorpair.rst │ ├── passivemotor.py │ ├── passivemotor.rst │ ├── tilt.py │ └── tiltsensor.rst ├── conf.py ├── hub_index.rst ├── license.rst ├── sphinx_selective_exclude │ ├── LICENSE │ ├── README.md │ ├── __init__.py │ ├── eager_only.py │ ├── modindex_exclude.py │ └── search_auto_exclude.py ├── sphinxcontrib │ └── cmtinc-buildhat.py ├── static │ ├── customstyle.css │ └── favicon.ico └── templates │ ├── layout.html │ ├── replace.inc │ ├── topindex.html │ └── versions.html ├── requirements-test.txt ├── setup.py └── test ├── color.py ├── distance.py ├── hat.py ├── light.py ├── matrix.py ├── motors.py └── wedo.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | docstring_style=sphinx 3 | max-line-length = 127 4 | ignore = D400, Q000, S311, PLW, PLC, PLR 5 | per-file-ignores = 6 | buildhat/__init__.py:F401 7 | exclude = docs/conf.py, docs/sphinxcontrib/cmtinc-buildhat.py, docs/sphinx_selective_exclude/*.py 8 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.9 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.9 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | - name: Install dependencies for testing 27 | run: | 28 | if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi 29 | - name: set pythonpath 30 | run: | 31 | echo "PYTHONPATH=." >> $GITHUB_ENV 32 | - name: Lint with flake8 33 | run: | 34 | flake8 . --version 35 | # stop the build if there are Python syntax errors or undefined names 36 | flake8 . --count --show-source --statistics 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | build/* 4 | *~ 5 | *.so 6 | *.egg-info 7 | dist/* 8 | # The venv running this installation is called "hat_env" 9 | # on the development machine. 10 | hat_env/* 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | .pypirc 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Environments 59 | .env 60 | .venv 61 | env/ 62 | venv/ 63 | ENV/ 64 | env.bak/ 65 | venv.bak/ 66 | 67 | # Pyre type checker 68 | .pyre/ 69 | 70 | # pytype static type analyzer 71 | .pytype/ 72 | 73 | # ide 74 | .idea/ 75 | *.iml 76 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/flake8 3 | rev: 6.0.0 4 | hooks: 5 | - id: flake8 6 | additional_dependencies: [ 7 | 'flake8-bugbear>=22.10.27', 8 | 'flake8-comprehensions>=3.10', 9 | 'flake8-debugger', 10 | 'flake8-docstrings>=1.6.0', 11 | 'flake8-isort>=5.0', 12 | 'flake8-pylint', 13 | 'flake8-rst-docstrings', 14 | 'flake8-string-format', 15 | 'gpiozero', 16 | 'pyserial' 17 | ] 18 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | image: latest 10 | 11 | python: 12 | version: 3.8 13 | install: 14 | - method: setuptools 15 | path: . 16 | 17 | # Build documentation in the docs/ directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | 21 | # Optionally build your docs in additional formats such as PDF 22 | # formats: 23 | # - pdf 24 | 25 | # Optionally declare the Python requirements required to build your docs 26 | # python: 27 | # install: 28 | # - requirements: docs/requirements.txt 29 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.9.7 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.7.0 4 | 5 | Adds: 6 | 7 | * Mode 7 IR transmission to ColorDistanceSensor https://github.com/RaspberryPiFoundation/python-build-hat/pull/205 8 | * Debug log filename access https://github.com/RaspberryPiFoundation/python-build-hat/pull/204 9 | * Movement counter to WeDo 2.0 Motion Sensor https://github.com/RaspberryPiFoundation/python-build-hat/pull/201 10 | 11 | ## 0.6.0 12 | 13 | ### Added 14 | 15 | * Support for Raspberry Pi 5 (https://github.com/RaspberryPiFoundation/python-build-hat/pull/203) 16 | 17 | ## 0.5.12 18 | 19 | ### Added 20 | 21 | * Ability to set custom device interval for callbacks 22 | 23 | ### Changed 24 | 25 | * New firmware with mitigation for motor disconnecting and selrate command 26 | 27 | ### Removed 28 | 29 | n/a 30 | 31 | ## 0.5.11 32 | 33 | ### Added 34 | 35 | * Expose release property to allow motor to hold position 36 | * Updated documentation 37 | 38 | ### Changed 39 | 40 | n/a 41 | 42 | ### Removed 43 | 44 | n/a 45 | 46 | ## 0.5.10 47 | 48 | ### Added 49 | 50 | * Support for 88008 motor 51 | * get_distance support for ColourDistanceSensor 52 | 53 | ### Changed 54 | 55 | * Use debug log level for logging 56 | 57 | ### Removed 58 | 59 | n/a 60 | 61 | ## 0.5.9 62 | 63 | ### Added 64 | 65 | * Allow the BuildHAT LEDs to be turned on and off 66 | * Allow for White in set\_pixels 67 | * Prevent using same port multiple times 68 | * Add support for checking maximum force sensed 69 | 70 | ### Changed 71 | 72 | * Linting of code 73 | * Renamed exceptions to conform to linter's style 74 | 75 | ### Removed 76 | 77 | n/a 78 | 79 | ## 0.5.8 80 | 81 | ### Added 82 | 83 | * LED Matrix transitions 84 | * Expose feature to measure voltage of hat 85 | * Function to set brightness of LEDs 86 | 87 | ### Changed 88 | 89 | * New firmware to fix passive devices in hardware revision 90 | 91 | ### Removed 92 | 93 | n/a 94 | 95 | ## 0.5.7 96 | 97 | ### Added 98 | 99 | * Support for light 100 | * Allow alternative serial device to be used 101 | * Passive motor support 102 | * WeDo sensor support 103 | 104 | ### Changed 105 | 106 | n/a 107 | 108 | ### Removed 109 | 110 | n/a 111 | 112 | ## 0.5.6 113 | 114 | ### Added 115 | 116 | 117 | ### Changed 118 | 119 | n/a 120 | 121 | ### Removed 122 | 123 | n/a 124 | 125 | ## 0.5.5 126 | 127 | ### Added 128 | 129 | 130 | ### Changed 131 | 132 | n/a 133 | 134 | ### Removed 135 | 136 | n/a 137 | 138 | ## 0.5.4 139 | 140 | ### Added 141 | 142 | Documentation copy updates 143 | 144 | ### Changed 145 | 146 | n/a 147 | 148 | ### Removed 149 | 150 | n/a 151 | 152 | ## 0.5.3 153 | 154 | ### Added 155 | 156 | * Force sensor now supports force threshold 157 | * Simplify list of colours supported by colour sensor 158 | * Fix distance sensor firing bug 159 | * Simplify coast call 160 | 161 | ### Changed 162 | 163 | n/a 164 | 165 | ### Removed 166 | 167 | n/a 168 | 169 | ## 0.5.2 170 | 171 | ### Added 172 | 173 | * Fixes force sensor callback only firing one 174 | * Pass distance to distance sensor callback 175 | * Make sure motors move simultaneously for a motor pair 176 | 177 | ### Changed 178 | 179 | n/a 180 | 181 | ### Removed 182 | 183 | n/a 184 | 185 | ## 0.5.1 186 | 187 | ### Added 188 | 189 | Further documentation better describing the Build HAT library components along with supported hardware. 190 | 191 | ### Changed 192 | 193 | n/a 194 | 195 | ### Removed 196 | 197 | n/a 198 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2021 - Raspberry Pi Foundation 4 | Copyright (c) 2017-2021 - LEGO System A/S - Aastvej 1, 7190 Billund, DK 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | recursive-include docs *.rst Makefile *.md *.py 3 | include docs/templates/*.html 4 | include docs/templates/*.inc 5 | include docs/static/* 6 | include VERSION 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build HAT 2 | 3 | The Python Build HAT library supports the Raspberry Pi Build HAT, an add-on board for the Raspberry Pi computer, which allows control of up to four LEGO® TECHNIC™ motors and sensors included in the SPIKE™ Portfolio. 4 | 5 | ## Hardware 6 | 7 | The Build HAT provides four connectors for LEGO® Technic™ motors and sensors from the SPIKE™ Portfolio. The available sensors include a distance sensor, a colour sensor, and a versatile force sensor. The angular motors come in a range of sizes and include integrated encoders that can be queried to find their position. 8 | 9 | The Build HAT fits all Raspberry Pi computers with a 40-pin GPIO header, including — with the addition of a ribbon cable or other extension device — Raspberry Pi 400. Connected LEGO® Technic™ devices can easily be controlled in Python, alongside standard Raspberry Pi accessories such as a camera module. 10 | 11 | ## Documentation 12 | 13 | Library documentation: https://buildhat.readthedocs.io 14 | 15 | Hardware documentation: https://www.raspberrypi.com/documentation/accessories/build-hat.html 16 | 17 | Projects and inspiration: https://projects.raspberrypi.org/en/pathways/lego-intro 18 | 19 | ## Installation 20 | 21 | To install the Build HAT library, enter the following commands in a terminal: 22 | 23 | pip3 install buildhat 24 | 25 | ## Usage 26 | 27 | See the [detailed documentation](https://buildhat.readthedocs.io/en/latest/buildhat/index.html) for the Python objects available. 28 | 29 | ```python 30 | import time 31 | from signal import pause 32 | from buildhat import Motor 33 | 34 | motor = Motor('A') 35 | motor.set_default_speed(30) 36 | 37 | print("Position", motor.get_aposition()) 38 | 39 | def handle_motor(speed, pos, apos): 40 | print("Motor", speed, pos, apos) 41 | 42 | motor.when_rotated = handle_motor 43 | 44 | print("Run for degrees") 45 | motor.run_for_degrees(360) 46 | 47 | print("Run for seconds") 48 | motor.run_for_seconds(5) 49 | 50 | print("Run for rotations") 51 | motor.run_for_rotations(2) 52 | 53 | print("Start motor") 54 | motor.start() 55 | time.sleep(3) 56 | print("Stop motor") 57 | motor.stop() 58 | 59 | pause() 60 | ``` 61 | 62 | ## Building locally 63 | 64 | Using [asdf](https://github.com/asdf-vm/asdf): 65 | 66 | ``` 67 | asdf install 68 | ``` 69 | 70 | Then: 71 | 72 | ``` 73 | pip3 install . --user 74 | ``` 75 | 76 | ### Building the documentation 77 | 78 | Instructions for regenerating the documentation can be found in 79 | `docs/README.md`. Briefly, assuming you have the appropriate python 80 | modules installed: 81 | 82 | ``` 83 | $ (cd docs; make html) 84 | ``` 85 | 86 | will rebuild the documentation. The doc tree starts at `docs/build/html/index.html` 87 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.0 2 | -------------------------------------------------------------------------------- /buildhat/__init__.py: -------------------------------------------------------------------------------- 1 | """Provide all the classes we need for build HAT""" 2 | 3 | from .color import ColorSensor 4 | from .colordistance import ColorDistanceSensor 5 | from .distance import DistanceSensor 6 | from .exc import * # noqa: F403 7 | from .force import ForceSensor 8 | from .hat import Hat 9 | from .light import Light 10 | from .matrix import Matrix 11 | from .motors import Motor, MotorPair, PassiveMotor 12 | from .serinterface import BuildHAT 13 | from .wedo import MotionSensor, TiltSensor 14 | -------------------------------------------------------------------------------- /buildhat/color.py: -------------------------------------------------------------------------------- 1 | """Color sensor handling functionality""" 2 | 3 | import math 4 | from collections import deque 5 | from threading import Condition 6 | 7 | from .devices import Device 8 | 9 | 10 | class ColorSensor(Device): 11 | """Color sensor 12 | 13 | :param port: Port of device 14 | :raises DeviceError: Occurs if there is no color sensor attached to port 15 | """ 16 | 17 | def __init__(self, port): 18 | """ 19 | Initialise color sensor 20 | 21 | :param port: Port of device 22 | """ 23 | super().__init__(port) 24 | self.reverse() 25 | self.mode(6) 26 | self.avg_reads = 4 27 | self._old_color = None 28 | 29 | def segment_color(self, r, g, b): 30 | """Return the color name from RGB 31 | 32 | :param r: Red 33 | :param g: Green 34 | :param b: Blue 35 | :return: Name of the color as a string 36 | :rtype: str 37 | """ 38 | table = [("black", (0, 0, 0)), 39 | ("violet", (127, 0, 255)), 40 | ("blue", (0, 0, 255)), 41 | ("cyan", (0, 183, 235)), 42 | ("green", (0, 128, 0)), 43 | ("yellow", (255, 255, 0)), 44 | ("red", (255, 0, 0)), 45 | ("white", (255, 255, 255))] 46 | near = "" 47 | euc = math.inf 48 | for itm in table: 49 | cur = math.sqrt((r - itm[1][0])**2 + (g - itm[1][1])**2 + (b - itm[1][2])**2) 50 | if cur < euc: 51 | near = itm[0] 52 | euc = cur 53 | return near 54 | 55 | def rgb_to_hsv(self, r, g, b): 56 | """Convert RGB to HSV 57 | 58 | Based on https://www.rapidtables.com/convert/color/rgb-to-hsv.html algorithm 59 | 60 | :param r: Red 61 | :param g: Green 62 | :param b: Blue 63 | :return: HSV representation of color 64 | :rtype: tuple 65 | """ 66 | r, g, b = r / 255.0, g / 255.0, b / 255.0 67 | cmax = max(r, g, b) 68 | cmin = min(r, g, b) 69 | delt = cmax - cmin 70 | if cmax == cmin: 71 | h = 0 72 | elif cmax == r: 73 | h = 60 * (((g - b) / delt) % 6) 74 | elif cmax == g: 75 | h = 60 * ((((b - r) / delt)) + 2) 76 | elif cmax == b: 77 | h = 60 * ((((r - g) / delt)) + 4) 78 | if cmax == 0: 79 | s = 0 80 | else: 81 | s = delt / cmax 82 | v = cmax 83 | return int(h), int(s * 100), int(v * 100) 84 | 85 | def get_color(self): 86 | """Return the color 87 | 88 | :return: Name of the color as a string 89 | :rtype: str 90 | """ 91 | r, g, b, _ = self.get_color_rgbi() 92 | return self.segment_color(r, g, b) 93 | 94 | def get_ambient_light(self): 95 | """Return the ambient light 96 | 97 | :return: Ambient light 98 | :rtype: int 99 | """ 100 | self.mode(2) 101 | readings = [] 102 | for _ in range(self.avg_reads): 103 | readings.append(self.get()[0]) 104 | return int(sum(readings) / len(readings)) 105 | 106 | def get_reflected_light(self): 107 | """Return the reflected light 108 | 109 | :return: Reflected light 110 | :rtype: int 111 | """ 112 | self.mode(1) 113 | readings = [] 114 | for _ in range(self.avg_reads): 115 | readings.append(self.get()[0]) 116 | return int(sum(readings) / len(readings)) 117 | 118 | def _avgrgbi(self, reads): 119 | readings = [] 120 | for read in reads: 121 | read = [int((read[0] / 1024) * 255), 122 | int((read[1] / 1024) * 255), 123 | int((read[2] / 1024) * 255), 124 | int((read[3] / 1024) * 255)] 125 | readings.append(read) 126 | rgbi = [] 127 | for i in range(4): 128 | rgbi.append(int(sum([rgbi[i] for rgbi in readings]) / len(readings))) 129 | return rgbi 130 | 131 | def get_color_rgbi(self): 132 | """Return the color 133 | 134 | :return: RGBI representation 135 | :rtype: list 136 | """ 137 | self.mode(5) 138 | reads = [] 139 | for _ in range(self.avg_reads): 140 | reads.append(self.get()) 141 | return self._avgrgbi(reads) 142 | 143 | def get_color_hsv(self): 144 | """Return the color 145 | 146 | :return: HSV representation 147 | :rtype: tuple 148 | """ 149 | self.mode(6) 150 | readings = [] 151 | for _ in range(self.avg_reads): 152 | read = self.get() 153 | read = [read[0], int((read[1] / 1024) * 100), int((read[2] / 1024) * 100)] 154 | readings.append(read) 155 | s = c = 0 156 | for hsv in readings: 157 | hue = hsv[0] 158 | s += math.sin(math.radians(hue)) 159 | c += math.cos(math.radians(hue)) 160 | 161 | hue = int((math.degrees((math.atan2(s, c))) + 360) % 360) 162 | sat = int(sum([hsv[1] for hsv in readings]) / len(readings)) 163 | val = int(sum([hsv[2] for hsv in readings]) / len(readings)) 164 | return (hue, sat, val) 165 | 166 | def _cb_handle(self, lst): 167 | self._data.append(lst[:4]) 168 | if len(self._data) == self.avg_reads: 169 | r, g, b, _ = self._avgrgbi(self._data) 170 | seg = self.segment_color(r, g, b) 171 | if self._cmp(seg, self._color): 172 | with self._cond: 173 | self._old_color = seg 174 | self._cond.notify() 175 | 176 | def wait_until_color(self, color): 177 | """Wait until specific color 178 | 179 | :param color: Color to look for 180 | """ 181 | self.mode(5) 182 | self._cond = Condition() 183 | self._data = deque(maxlen=self.avg_reads) 184 | self._color = color 185 | self._cmp = lambda x, y: x == y 186 | self.callback(self._cb_handle) 187 | with self._cond: 188 | self._cond.wait() 189 | self.callback(None) 190 | 191 | def wait_for_new_color(self): 192 | """Wait for new color or returns immediately if first call 193 | 194 | :return: Name of the color as a string 195 | :rtype: str 196 | """ 197 | self.mode(5) 198 | if self._old_color is None: 199 | self._old_color = self.get_color() 200 | return self._old_color 201 | self._cond = Condition() 202 | self._data = deque(maxlen=self.avg_reads) 203 | self._color = self._old_color 204 | self._cmp = lambda x, y: x != y 205 | self.callback(self._cb_handle) 206 | with self._cond: 207 | self._cond.wait() 208 | self.callback(None) 209 | return self._old_color 210 | 211 | def on(self): 212 | """Turn on the sensor and LED""" 213 | self.reverse() 214 | -------------------------------------------------------------------------------- /buildhat/colordistance.py: -------------------------------------------------------------------------------- 1 | """Color distance sensor handling functionality""" 2 | 3 | import math 4 | from collections import deque 5 | from threading import Condition 6 | 7 | from .devices import Device 8 | 9 | 10 | class ColorDistanceSensor(Device): 11 | """Color Distance sensor 12 | 13 | :param port: Port of device 14 | :raises DeviceError: Occurs if there is no colordistance sensor attached to port 15 | """ 16 | 17 | def __init__(self, port): 18 | """ 19 | Initialise color distance sensor 20 | 21 | :param port: Port of device 22 | """ 23 | super().__init__(port) 24 | self.on() 25 | self.mode(6) 26 | self.avg_reads = 4 27 | self._old_color = None 28 | self._ir_channel = 0x0 29 | self._ir_address = 0x0 30 | self._ir_toggle = 0x0 31 | 32 | def segment_color(self, r, g, b): 33 | """Return the color name from HSV 34 | 35 | :param r: Red 36 | :param g: Green 37 | :param b: Blue 38 | :return: Name of the color as a string 39 | :rtype: str 40 | """ 41 | table = [("black", (0, 0, 0)), 42 | ("violet", (127, 0, 255)), 43 | ("blue", (0, 0, 255)), 44 | ("cyan", (0, 183, 235)), 45 | ("green", (0, 128, 0)), 46 | ("yellow", (255, 255, 0)), 47 | ("red", (255, 0, 0)), 48 | ("white", (255, 255, 255))] 49 | near = "" 50 | euc = math.inf 51 | for itm in table: 52 | cur = math.sqrt((r - itm[1][0]) ** 2 + (g - itm[1][1]) ** 2 + (b - itm[1][2]) ** 2) 53 | if cur < euc: 54 | near = itm[0] 55 | euc = cur 56 | return near 57 | 58 | def rgb_to_hsv(self, r, g, b): 59 | """Convert RGB to HSV 60 | 61 | Based on https://www.rapidtables.com/convert/color/rgb-to-hsv.html algorithm 62 | 63 | :param r: Red 64 | :param g: Green 65 | :param b: Blue 66 | :return: HSV representation of color 67 | :rtype: tuple 68 | """ 69 | r, g, b = r / 255.0, g / 255.0, b / 255.0 70 | cmax = max(r, g, b) 71 | cmin = min(r, g, b) 72 | delt = cmax - cmin 73 | if cmax == cmin: 74 | h = 0 75 | elif cmax == r: 76 | h = 60 * (((g - b) / delt) % 6) 77 | elif cmax == g: 78 | h = 60 * ((((b - r) / delt)) + 2) 79 | elif cmax == b: 80 | h = 60 * ((((r - g) / delt)) + 4) 81 | if cmax == 0: 82 | s = 0 83 | else: 84 | s = delt / cmax 85 | v = cmax 86 | return int(h), int(s * 100), int(v * 100) 87 | 88 | def get_color(self): 89 | """Return the color 90 | 91 | :return: Name of the color as a string 92 | :rtype: str 93 | """ 94 | r, g, b = self.get_color_rgb() 95 | return self.segment_color(r, g, b) 96 | 97 | def get_ambient_light(self): 98 | """Return the ambient light 99 | 100 | :return: Ambient light 101 | :rtype: int 102 | """ 103 | self.mode(4) 104 | readings = [] 105 | for _ in range(self.avg_reads): 106 | readings.append(self.get()[0]) 107 | return int(sum(readings) / len(readings)) 108 | 109 | def get_reflected_light(self): 110 | """Return the reflected light 111 | 112 | :return: Reflected light 113 | :rtype: int 114 | """ 115 | self.mode(3) 116 | readings = [] 117 | for _ in range(self.avg_reads): 118 | readings.append(self.get()[0]) 119 | return int(sum(readings) / len(readings)) 120 | 121 | def get_distance(self): 122 | """Return the distance 123 | 124 | :return: Distance 125 | :rtype: int 126 | """ 127 | self.mode(1) 128 | distance = self.get()[0] 129 | return distance 130 | 131 | def _clamp(self, val, small, large): 132 | return max(small, min(val, large)) 133 | 134 | def _avgrgb(self, reads): 135 | readings = [] 136 | for read in reads: 137 | read = [int((self._clamp(read[0], 0, 400) / 400) * 255), 138 | int((self._clamp(read[1], 0, 400) / 400) * 255), 139 | int((self._clamp(read[2], 0, 400) / 400) * 255)] 140 | readings.append(read) 141 | rgb = [] 142 | for i in range(3): 143 | rgb.append(int(sum([rgb[i] for rgb in readings]) / len(readings))) 144 | return rgb 145 | 146 | def get_color_rgb(self): 147 | """Return the color 148 | 149 | :return: RGBI representation 150 | :rtype: list 151 | """ 152 | self.mode(6) 153 | reads = [] 154 | for _ in range(self.avg_reads): 155 | reads.append(self.get()) 156 | return self._avgrgb(reads) 157 | 158 | def _cb_handle(self, lst): 159 | self._data.append(lst) 160 | if len(self._data) == self.avg_reads: 161 | r, g, b = self._avgrgb(self._data) 162 | seg = self.segment_color(r, g, b) 163 | if self._cmp(seg, self._color): 164 | with self._cond: 165 | self._old_color = seg 166 | self._cond.notify() 167 | 168 | def wait_until_color(self, color): 169 | """Wait until specific color 170 | 171 | :param color: Color to look for 172 | """ 173 | self.mode(6) 174 | self._cond = Condition() 175 | self._data = deque(maxlen=self.avg_reads) 176 | self._color = color 177 | self._cmp = lambda x, y: x == y 178 | self.callback(self._cb_handle) 179 | with self._cond: 180 | self._cond.wait() 181 | self.callback(None) 182 | 183 | def wait_for_new_color(self): 184 | """Wait for new color or returns immediately if first call 185 | 186 | :return: Name of the color as a string 187 | :rtype: str 188 | """ 189 | self.mode(6) 190 | if self._old_color is None: 191 | self._old_color = self.get_color() 192 | return self._old_color 193 | self._cond = Condition() 194 | self._data = deque(maxlen=self.avg_reads) 195 | self._color = self._old_color 196 | self._cmp = lambda x, y: x != y 197 | self.callback(self._cb_handle) 198 | with self._cond: 199 | self._cond.wait() 200 | self.callback(None) 201 | return self._old_color 202 | 203 | @property 204 | def ir_channel(self): 205 | """Get the IR channel for message transmission""" 206 | return self._ir_channel 207 | 208 | @ir_channel.setter 209 | def ir_channel(self, channel=1): 210 | """ 211 | Set the IR channel for RC Tx 212 | 213 | :param channel: 1-4 indicating the selected IR channel on the reciever 214 | """ 215 | check_chan = channel 216 | if check_chan > 4: 217 | check_chan = 4 218 | elif check_chan < 1: 219 | check_chan = 1 220 | # Internally: 0-3 221 | self._ir_channel = int(check_chan) - 1 222 | 223 | @property 224 | def ir_address(self): 225 | """IR Address space of 0x0 for default PoweredUp or 0x1 for extra space""" 226 | return self._ir_address 227 | 228 | def toggle_ir_toggle(self): 229 | """Toggle the IR toggle bit""" 230 | # IYKYK, because the RC documents are not clear 231 | if self._ir_toggle: 232 | self._ir_toggle = 0x0 233 | else: 234 | self._ir_toggle = 0x1 235 | return self._ir_toggle 236 | 237 | def send_ir_sop(self, port, mode): 238 | """ 239 | Send an IR message via Power Functions RC Protocol in Single Output PWM mode 240 | 241 | PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm 242 | 243 | Port B is blue 244 | 245 | Valid values for mode are: 246 | 0x0: Float output 247 | 0x1: Forward/Clockwise at speed 1 248 | 0x2: Forward/Clockwise at speed 2 249 | 0x3: Forward/Clockwise at speed 3 250 | 0x4: Forward/Clockwise at speed 4 251 | 0x5: Forward/Clockwise at speed 5 252 | 0x6: Forward/Clockwise at speed 6 253 | 0x7: Forward/Clockwise at speed 7 254 | 0x8: Brake (then float v1.20) 255 | 0x9: Backwards/Counterclockwise at speed 7 256 | 0xA: Backwards/Counterclockwise at speed 6 257 | 0xB: Backwards/Counterclockwise at speed 5 258 | 0xC: Backwards/Counterclockwise at speed 4 259 | 0xD: Backwards/Counterclockwise at speed 3 260 | 0xE: Backwards/Counterclockwise at speed 2 261 | 0xF: Backwards/Counterclockwise at speed 1 262 | 263 | :param port: 'A' or 'B' 264 | :param mode: 0-15 indicating the port's mode to set 265 | """ 266 | escape_modeselect = 0x0 267 | escape = escape_modeselect 268 | 269 | ir_mode_single_output = 0x4 270 | ir_mode = ir_mode_single_output 271 | 272 | so_mode_pwm = 0x0 273 | so_mode = so_mode_pwm 274 | 275 | output_port_a = 0x0 276 | output_port_b = 0x1 277 | 278 | output_port = None 279 | if port == 'A' or port == 'a': 280 | output_port = output_port_a 281 | elif port == 'B' or port == 'b': 282 | output_port = output_port_b 283 | else: 284 | return False 285 | 286 | ir_mode = ir_mode | (so_mode << 1) | output_port 287 | 288 | nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel 289 | nibble2 = (self._ir_address << 3) | ir_mode 290 | 291 | # Mode range checked here 292 | return self._send_ir_nibbles(nibble1, nibble2, mode) 293 | 294 | def send_ir_socstid(self, port, mode): 295 | """ 296 | Send an IR message via Power Functions RC Protocol in Single Output Clear/Set/Toggle/Increment/Decrement mode 297 | 298 | PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm 299 | 300 | Valid values for mode are: 301 | 0x0: Toggle full Clockwise/Forward (Stop to Clockwise, Clockwise to Stop, Counterclockwise to Clockwise) 302 | 0x1: Toggle direction 303 | 0x2: Increment numerical PWM 304 | 0x3: Decrement numerical PWM 305 | 0x4: Increment PWM 306 | 0x5: Decrement PWM 307 | 0x6: Full Clockwise/Forward 308 | 0x7: Full Counterclockwise/Backward 309 | 0x8: Toggle full (defaults to Forward, first) 310 | 0x9: Clear C1 (C1 to High) 311 | 0xA: Set C1 (C1 to Low) 312 | 0xB: Toggle C1 313 | 0xC: Clear C2 (C2 to High) 314 | 0xD: Set C2 (C2 to Low) 315 | 0xE: Toggle C2 316 | 0xF: Toggle full Counterclockwise/Backward (Stop to Clockwise, Counterclockwise to Stop, Clockwise to Counterclockwise) 317 | 318 | :param port: 'A' or 'B' 319 | :param mode: 0-15 indicating the port's mode to set 320 | """ 321 | escape_modeselect = 0x0 322 | escape = escape_modeselect 323 | 324 | ir_mode_single_output = 0x4 325 | ir_mode = ir_mode_single_output 326 | 327 | so_mode_cstid = 0x1 328 | so_mode = so_mode_cstid 329 | 330 | output_port_a = 0x0 331 | output_port_b = 0x1 332 | 333 | output_port = None 334 | if port == 'A' or port == 'a': 335 | output_port = output_port_a 336 | elif port == 'B' or port == 'b': 337 | output_port = output_port_b 338 | else: 339 | return False 340 | 341 | ir_mode = ir_mode | (so_mode << 1) | output_port 342 | 343 | nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel 344 | nibble2 = (self._ir_address << 3) | ir_mode 345 | 346 | # Mode range checked here 347 | return self._send_ir_nibbles(nibble1, nibble2, mode) 348 | 349 | def send_ir_combo_pwm(self, port_b_mode, port_a_mode): 350 | """ 351 | Send an IR message via Power Functions RC Protocol in Combo PWM mode 352 | 353 | PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm 354 | 355 | Valid values for the modes are: 356 | 0x0 Float 357 | 0x1 PWM Forward step 1 358 | 0x2 PWM Forward step 2 359 | 0x3 PWM Forward step 3 360 | 0x4 PWM Forward step 4 361 | 0x5 PWM Forward step 5 362 | 0x6 PWM Forward step 6 363 | 0x7 PWM Forward step 7 364 | 0x8 Brake (then float v1.20) 365 | 0x9 PWM Backward step 7 366 | 0xA PWM Backward step 6 367 | 0xB PWM Backward step 5 368 | 0xC PWM Backward step 4 369 | 0xD PWM Backward step 3 370 | 0xE PWM Backward step 2 371 | 0xF PWM Backward step 1 372 | 373 | :param port_b_mode: 0-15 indicating the command to send to port B 374 | :param port_a_mode: 0-15 indicating the command to send to port A 375 | """ 376 | escape_combo_pwm = 0x1 377 | escape = escape_combo_pwm 378 | 379 | nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel 380 | 381 | # Port modes are range checked here 382 | return self._send_ir_nibbles(nibble1, port_b_mode, port_a_mode) 383 | 384 | def send_ir_combo_direct(self, port_b_output, port_a_output): 385 | """ 386 | Send an IR message via Power Functions RC Protocol in Combo Direct mode 387 | 388 | PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm 389 | 390 | Valid values for the output variables are: 391 | 0x0: Float output 392 | 0x1: Clockwise/Forward 393 | 0x2: Counterclockwise/Backwards 394 | 0x3: Brake then float 395 | 396 | :param port_b_output: 0-3 indicating the output to send to port B 397 | :param port_a_output: 0-3 indicating the output to send to port A 398 | """ 399 | escape_modeselect = 0x0 400 | escape = escape_modeselect 401 | 402 | ir_mode_combo_direct = 0x1 403 | ir_mode = ir_mode_combo_direct 404 | 405 | nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel 406 | nibble2 = (self._ir_address << 3) | ir_mode 407 | 408 | if port_b_output > 0x3 or port_a_output > 0x3: 409 | return False 410 | if port_b_output < 0x0 or port_a_output < 0x0: 411 | return False 412 | 413 | nibble3 = (port_b_output << 2) | port_a_output 414 | 415 | return self._send_ir_nibbles(nibble1, nibble2, nibble3) 416 | 417 | def send_ir_extended(self, mode): 418 | """ 419 | Send an IR message via Power Functions RC Protocol in Extended mode 420 | 421 | PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm 422 | 423 | Valid values for the mode are: 424 | 0x0: Brake Port A (timeout) 425 | 0x1: Increment Speed on Port A 426 | 0x2: Decrement Speed on Port A 427 | 428 | 0x4: Toggle Forward/Clockwise/Float on Port B 429 | 430 | 0x6: Toggle Address bit 431 | 0x7: Align toggle bit 432 | 433 | :param mode: 0-2,4,6-7 434 | """ 435 | escape_modeselect = 0x0 436 | escape = escape_modeselect 437 | 438 | ir_mode_extended = 0x0 439 | ir_mode = ir_mode_extended 440 | 441 | nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel 442 | nibble2 = (self._ir_address << 3) | ir_mode 443 | 444 | if mode < 0x0 or mode == 0x3 or mode == 0x5 or mode > 0x7: 445 | return False 446 | 447 | return self._send_ir_nibbles(nibble1, nibble2, mode) 448 | 449 | def send_ir_single_pin(self, port, pin, mode, timeout): 450 | """ 451 | Send an IR message via Power Functions RC Protocol in Single Pin mode 452 | 453 | PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm 454 | 455 | Valid values for the mode are: 456 | 0x0: No-op 457 | 0x1: Clear 458 | 0x2: Set 459 | 0x3: Toggle 460 | 461 | Note: The unlabeled IR receiver (vs the one labeled V2) has a "firmware bug in Single Pin mode" 462 | https://www.philohome.com/pfrec/pfrec.htm 463 | 464 | :param port: 'A' or 'B' 465 | :param pin: 1 or 2 466 | :param mode: 0-3 indicating the pin's mode to set 467 | :param timeout: True or False 468 | """ 469 | escape_mode = 0x0 470 | escape = escape_mode 471 | 472 | ir_mode_single_continuous = 0x2 473 | ir_mode_single_timeout = 0x3 474 | ir_mode = None 475 | if timeout: 476 | ir_mode = ir_mode_single_timeout 477 | else: 478 | ir_mode = ir_mode_single_continuous 479 | 480 | output_port_a = 0x0 481 | output_port_b = 0x1 482 | 483 | output_port = None 484 | if port == 'A' or port == 'a': 485 | output_port = output_port_a 486 | elif port == 'B' or port == 'b': 487 | output_port = output_port_b 488 | else: 489 | return False 490 | 491 | if pin != 1 and pin != 2: 492 | return False 493 | pin_value = pin - 1 494 | 495 | if mode > 0x3 or mode < 0x0: 496 | return False 497 | 498 | nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel 499 | nibble2 = (self._ir_address << 3) | ir_mode 500 | nibble3 = (output_port << 3) | (pin_value << 3) | mode 501 | 502 | return self._send_ir_nibbles(nibble1, nibble2, nibble3) 503 | 504 | def _send_ir_nibbles(self, nibble1, nibble2, nibble3): 505 | 506 | # M7 IR Tx SI = N/A 507 | # format count=1 type=1 chars=5 dp=0 508 | # RAW: 00000000 0000FFFF PCT: 00000000 00000064 SI: 00000000 0000FFFF 509 | 510 | mode = 7 511 | self.mode(mode) 512 | 513 | # The upper bits of data[2] are ignored 514 | if nibble1 > 0xF or nibble2 > 0xF or nibble3 > 0xF: 515 | return False 516 | if nibble1 < 0x0 or nibble2 < 0x0 or nibble3 < 0x0: 517 | return False 518 | 519 | byte_two = (nibble2 << 4) | nibble3 520 | 521 | data = bytearray(3) 522 | data[0] = (0xc << 4) | mode 523 | data[1] = byte_two 524 | data[2] = nibble1 525 | 526 | # print(" ".join('{:04b}'.format(nibble1))) 527 | # print(" ".join('{:04b}'.format(nibble2))) 528 | # print(" ".join('{:04b}'.format(nibble3))) 529 | # print(" ".join('{:08b}'.format(n) for n in data)) 530 | 531 | self._write1(data) 532 | return True 533 | 534 | def on(self): 535 | """Turn on the sensor and LED""" 536 | self.reverse() 537 | -------------------------------------------------------------------------------- /buildhat/data/firmware.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/buildhat/data/firmware.bin -------------------------------------------------------------------------------- /buildhat/data/signature.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/buildhat/data/signature.bin -------------------------------------------------------------------------------- /buildhat/data/version: -------------------------------------------------------------------------------- 1 | 1674818421 2 | -------------------------------------------------------------------------------- /buildhat/devices.py: -------------------------------------------------------------------------------- 1 | """Functionality for handling Build HAT devices""" 2 | 3 | import os 4 | import sys 5 | import weakref 6 | from concurrent.futures import Future 7 | 8 | from .exc import DeviceError 9 | from .serinterface import BuildHAT 10 | 11 | 12 | class Device: 13 | """Creates a single instance of the buildhat for all devices to use""" 14 | 15 | _instance = None 16 | _started = 0 17 | _device_names = {1: ("PassiveMotor", "PassiveMotor"), 18 | 2: ("PassiveMotor", "PassiveMotor"), 19 | 8: ("Light", "Light"), # 88005 20 | 34: ("TiltSensor", "WeDo 2.0 Tilt Sensor"), # 45305 21 | 35: ("MotionSensor", "MotionSensor"), # 45304 22 | 37: ("ColorDistanceSensor", "Color & Distance Sensor"), # 88007 23 | 61: ("ColorSensor", "Color Sensor"), # 45605 24 | 62: ("DistanceSensor", "Distance Sensor"), # 45604 25 | 63: ("ForceSensor", "Force Sensor"), # 45606 26 | 64: ("Matrix", "3x3 Color Light Matrix"), # 45608 27 | 38: ("Motor", "Medium Linear Motor"), # 88008 28 | 46: ("Motor", "Large Motor"), # 88013 29 | 47: ("Motor", "XL Motor"), # 88014 30 | 48: ("Motor", "Medium Angular Motor (Cyan)"), # 45603 31 | 49: ("Motor", "Large Angular Motor (Cyan)"), # 45602 32 | 65: ("Motor", "Small Angular Motor"), # 45607 33 | 75: ("Motor", "Medium Angular Motor (Grey)"), # 88018 34 | 76: ("Motor", "Large Angular Motor (Grey)")} # 88017 35 | 36 | _used = {0: False, 37 | 1: False, 38 | 2: False, 39 | 3: False} 40 | 41 | UNKNOWN_DEVICE = "Unknown" 42 | DISCONNECTED_DEVICE = "Disconnected" 43 | 44 | def __init__(self, port): 45 | """Initialise device 46 | 47 | :param port: Port of device 48 | :raises DeviceError: Occurs if incorrect port specified or port already used 49 | """ 50 | if not isinstance(port, str) or len(port) != 1: 51 | raise DeviceError("Invalid port") 52 | p = ord(port) - ord('A') 53 | if not (p >= 0 and p <= 3): 54 | raise DeviceError("Invalid port") 55 | if Device._used[p]: 56 | raise DeviceError("Port already used") 57 | self.port = p 58 | Device._setup() 59 | self._simplemode = -1 60 | self._combimode = -1 61 | self._modestr = "" 62 | self._typeid = self._conn.typeid 63 | self._interval = 10 64 | if ( 65 | self._typeid in Device._device_names 66 | and Device._device_names[self._typeid][0] != type(self).__name__ # noqa: W503 67 | ) or self._typeid == -1: 68 | raise DeviceError(f'There is not a {type(self).__name__} connected to port {port} (Found {self.name})') 69 | Device._used[p] = True 70 | 71 | @staticmethod 72 | def _setup(**kwargs): 73 | if Device._instance: 74 | return 75 | data = os.path.join(os.path.dirname(sys.modules["buildhat"].__file__), "data/") 76 | firm = os.path.join(data, "firmware.bin") 77 | sig = os.path.join(data, "signature.bin") 78 | ver = os.path.join(data, "version") 79 | vfile = open(ver) 80 | v = int(vfile.read()) 81 | vfile.close() 82 | Device._instance = BuildHAT(firm, sig, v, **kwargs) 83 | weakref.finalize(Device._instance, Device._instance.shutdown) 84 | 85 | def __del__(self): 86 | """Handle deletion of device""" 87 | if hasattr(self, "port") and Device._used[self.port]: 88 | Device._used[self.port] = False 89 | self._conn.callit = None 90 | self.deselect() 91 | self.off() 92 | 93 | @staticmethod 94 | def name_for_id(typeid): 95 | """Translate integer type id to device name (python class) 96 | 97 | :param typeid: Type of device 98 | :return: Name of device 99 | """ 100 | if typeid in Device._device_names: 101 | return Device._device_names[typeid][0] 102 | else: 103 | return Device.UNKNOWN_DEVICE 104 | 105 | @staticmethod 106 | def desc_for_id(typeid): 107 | """Translate integer type id to something more descriptive than the device name 108 | 109 | :param typeid: Type of device 110 | :return: Description of device 111 | """ 112 | if typeid in Device._device_names: 113 | return Device._device_names[typeid][1] 114 | else: 115 | return Device.UNKNOWN_DEVICE 116 | 117 | @property 118 | def _conn(self): 119 | return Device._instance.connections[self.port] 120 | 121 | @property 122 | def connected(self): 123 | """Whether device is connected or not 124 | 125 | :return: Connection status 126 | """ 127 | return self._conn.connected 128 | 129 | @property 130 | def typeid(self): 131 | """Type ID of device 132 | 133 | :return: Type ID 134 | """ 135 | return self._typeid 136 | 137 | @property 138 | def typeidcur(self): 139 | """Type ID currently present 140 | 141 | :return: Type ID 142 | """ 143 | return self._conn.typeid 144 | 145 | @property 146 | def _hat(self): 147 | """Hat instance 148 | 149 | :return: Hat instance 150 | """ 151 | return Device._instance 152 | 153 | @property 154 | def name(self): 155 | """Determine name of device on port 156 | 157 | :return: Device name 158 | """ 159 | if not self.connected: 160 | return Device.DISCONNECTED_DEVICE 161 | elif self.typeidcur in self._device_names: 162 | return self._device_names[self.typeidcur][0] 163 | else: 164 | return Device.UNKNOWN_DEVICE 165 | 166 | @property 167 | def description(self): 168 | """Device on port info 169 | 170 | :return: Device description 171 | """ 172 | if not self.connected: 173 | return Device.DISCONNECTED_DEVICE 174 | elif self.typeidcur in self._device_names: 175 | return self._device_names[self.typeidcur][1] 176 | else: 177 | return Device.UNKNOWN_DEVICE 178 | 179 | def isconnected(self): 180 | """Whether it is connected or not 181 | 182 | :raises DeviceError: Occurs if device no longer the same 183 | """ 184 | if not self.connected: 185 | raise DeviceError("No device found") 186 | if self.typeid != self.typeidcur: 187 | raise DeviceError("Device has changed") 188 | 189 | def reverse(self): 190 | """Reverse polarity""" 191 | self._write(f"port {self.port} ; port_plimit 1 ; set -1\r") 192 | 193 | def get(self): 194 | """Extract information from device 195 | 196 | :return: Data from device 197 | :raises DeviceError: Occurs if device not in valid mode 198 | """ 199 | self.isconnected() 200 | if self._simplemode == -1 and self._combimode == -1: 201 | raise DeviceError("Not in simple or combimode") 202 | ftr = Future() 203 | self._hat.portftr[self.port].append(ftr) 204 | return ftr.result() 205 | 206 | def mode(self, modev): 207 | """Set combimode or simple mode 208 | 209 | :param modev: List of tuples for a combimode, or integer for simple mode 210 | """ 211 | self.isconnected() 212 | if isinstance(modev, list): 213 | modestr = "" 214 | for t in modev: 215 | modestr += f"{t[0]} {t[1]} " 216 | if self._simplemode == -1 and self._combimode == 0 and self._modestr == modestr: 217 | return 218 | self._write(f"port {self.port}; select\r") 219 | self._combimode = 0 220 | self._write((f"port {self.port} ; combi {self._combimode} {modestr} ; " 221 | f"select {self._combimode} ; " 222 | f"selrate {self._interval}\r")) 223 | self._simplemode = -1 224 | self._modestr = modestr 225 | self._conn.combimode = 0 226 | self._conn.simplemode = -1 227 | else: 228 | if self._combimode == -1 and self._simplemode == int(modev): 229 | return 230 | # Remove combi mode 231 | if self._combimode != -1: 232 | self._write(f"port {self.port} ; combi {self._combimode}\r") 233 | self._write(f"port {self.port}; select\r") 234 | self._combimode = -1 235 | self._simplemode = int(modev) 236 | self._write(f"port {self.port} ; select {int(modev)} ; selrate {self._interval}\r") 237 | self._conn.combimode = -1 238 | self._conn.simplemode = int(modev) 239 | 240 | def select(self): 241 | """Request data from mode 242 | 243 | :raises DeviceError: Occurs if device not in valid mode 244 | """ 245 | self.isconnected() 246 | if self._simplemode != -1: 247 | idx = self._simplemode 248 | elif self._combimode != -1: 249 | idx = self._combimode 250 | else: 251 | raise DeviceError("Not in simple or combimode") 252 | self._write(f"port {self.port} ; select {idx} ; selrate {self._interval}\r") 253 | 254 | def on(self): 255 | """Turn on sensor""" 256 | self._write(f"port {self.port} ; port_plimit 1 ; on\r") 257 | 258 | def off(self): 259 | """Turn off sensor""" 260 | self._write(f"port {self.port} ; off\r") 261 | 262 | def deselect(self): 263 | """Unselect data from mode""" 264 | self._write(f"port {self.port} ; select\r") 265 | 266 | def _write(self, cmd): 267 | self.isconnected() 268 | Device._instance.write(cmd.encode()) 269 | 270 | def _write1(self, data): 271 | hexstr = ' '.join(f'{h:x}' for h in data) 272 | self._write(f"port {self.port} ; write1 {hexstr}\r") 273 | 274 | def callback(self, func): 275 | """Set callback function 276 | 277 | :param func: Callback function 278 | """ 279 | if func is not None: 280 | self.select() 281 | else: 282 | self.deselect() 283 | if func is None: 284 | self._conn.callit = None 285 | else: 286 | self._conn.callit = weakref.WeakMethod(func) 287 | 288 | @property 289 | def interval(self): 290 | """Interval between data points in milliseconds 291 | 292 | :getter: Gets interval 293 | :setter: Sets interval 294 | :return: Device interval 295 | :rtype: int 296 | """ 297 | return self._interval 298 | 299 | @interval.setter 300 | def interval(self, value): 301 | """Interval between data points in milliseconds 302 | 303 | :param value: Interval 304 | :type value: int 305 | :raises DeviceError: Occurs if invalid interval passed 306 | """ 307 | if isinstance(value, int) and value >= 0 and value <= 1000000000: 308 | self._interval = value 309 | self._write(f"port {self.port} ; selrate {self._interval}\r") 310 | else: 311 | raise DeviceError("Invalid interval") 312 | -------------------------------------------------------------------------------- /buildhat/distance.py: -------------------------------------------------------------------------------- 1 | """Distance sensor handling functionality""" 2 | 3 | from threading import Condition 4 | 5 | from .devices import Device 6 | from .exc import DistanceSensorError 7 | 8 | 9 | class DistanceSensor(Device): 10 | """Distance sensor 11 | 12 | :param port: Port of device 13 | :raises DeviceError: Occurs if there is no distance sensor attached to port 14 | """ 15 | 16 | def __init__(self, port, threshold_distance=100): 17 | """ 18 | Initialise distance sensor 19 | 20 | :param port: Port of device 21 | :param threshold_distance: Optional 22 | """ 23 | super().__init__(port) 24 | self.on() 25 | self.mode(0) 26 | self._cond_data = Condition() 27 | self._when_in_range = None 28 | self._when_out_of_range = None 29 | self._fired_in = False 30 | self._fired_out = False 31 | self._threshold_distance = threshold_distance 32 | self._distance = -1 33 | 34 | def _intermediate(self, data): 35 | self._distance = data[0] 36 | if self._distance != -1 and self._distance < self.threshold_distance and not self._fired_in: 37 | if self._when_in_range is not None: 38 | self._when_in_range(data[0]) 39 | self._fired_in = True 40 | self._fired_out = False 41 | if self._distance != -1 and self._distance > self.threshold_distance and not self._fired_out: 42 | if self._when_out_of_range is not None: 43 | self._when_out_of_range(data[0]) 44 | self._fired_in = False 45 | self._fired_out = True 46 | with self._cond_data: 47 | self._data = data[0] 48 | self._cond_data.notify() 49 | 50 | @property 51 | def distance(self): 52 | """ 53 | Obtain previously stored distance 54 | 55 | :getter: Returns distance 56 | :return: Stored distance 57 | """ 58 | return self._distance 59 | 60 | @property 61 | def threshold_distance(self): 62 | """ 63 | Threshold distance value 64 | 65 | :getter: Returns threshold distance 66 | :setter: Sets threshold distance 67 | :return: Threshold distance 68 | """ 69 | return self._threshold_distance 70 | 71 | @threshold_distance.setter 72 | def threshold_distance(self, value): 73 | self._threshold_distance = value 74 | 75 | def get_distance(self): 76 | """ 77 | Return the distance from ultrasonic sensor to object 78 | 79 | :return: Distance from ultrasonic sensor 80 | :rtype: int 81 | """ 82 | dist = self.get()[0] 83 | return dist 84 | 85 | @property 86 | def when_in_range(self): 87 | """ 88 | Handle motion events 89 | 90 | :getter: Returns function to be called when in range 91 | :setter: Sets function to be called when in range 92 | :return: In range callback 93 | """ 94 | return self._when_in_range 95 | 96 | @when_in_range.setter 97 | def when_in_range(self, value): 98 | """Call back, when distance in range 99 | 100 | :param value: In range callback 101 | """ 102 | self._when_in_range = value 103 | self.callback(self._intermediate) 104 | 105 | @property 106 | def when_out_of_range(self): 107 | """ 108 | Handle motion events 109 | 110 | :getter: Returns function to be called when out of range 111 | :setter: Sets function to be called when out of range 112 | :return: Out of range callback 113 | """ 114 | return self._when_out_of_range 115 | 116 | @when_out_of_range.setter 117 | def when_out_of_range(self, value): 118 | """Call back, when distance out of range 119 | 120 | :param value: Out of range callback 121 | """ 122 | self._when_out_of_range = value 123 | self.callback(self._intermediate) 124 | 125 | def wait_for_out_of_range(self, distance): 126 | """Wait until object is farther than specified distance 127 | 128 | :param distance: Distance 129 | """ 130 | self.callback(self._intermediate) 131 | with self._cond_data: 132 | self._cond_data.wait() 133 | while self._data < distance: 134 | self._cond_data.wait() 135 | 136 | def wait_for_in_range(self, distance): 137 | """Wait until object is closer than specified distance 138 | 139 | :param distance: Distance 140 | """ 141 | self.callback(self._intermediate) 142 | with self._cond_data: 143 | self._cond_data.wait() 144 | while self._data == -1 or self._data > distance: 145 | self._cond_data.wait() 146 | 147 | def eyes(self, *args): 148 | """ 149 | Brightness of LEDs on sensor 150 | 151 | (Sensor Right Upper, Sensor Left Upper, Sensor Right Lower, Sensor Left Lower) 152 | 153 | :param args: Four brightness arguments of 0 to 100 154 | :raises DistanceSensorError: Occurs if invalid brightness passed 155 | """ 156 | out = [0xc5] 157 | if len(args) != 4: 158 | raise DistanceSensorError("Need 4 brightness args, of 0 to 100") 159 | for v in args: 160 | if not (v >= 0 and v <= 100): 161 | raise DistanceSensorError("Need 4 brightness args, of 0 to 100") 162 | out += [v] 163 | self._write1(out) 164 | 165 | def on(self): 166 | """Turn on the sensor""" 167 | self._write(f"port {self.port} ; set -1\r") 168 | -------------------------------------------------------------------------------- /buildhat/exc.py: -------------------------------------------------------------------------------- 1 | """Exceptions for all build HAT classes""" 2 | 3 | 4 | class DistanceSensorError(Exception): 5 | """Error raised when invalid arguments passed to distance sensor functions""" 6 | 7 | 8 | class MatrixError(Exception): 9 | """Error raised when invalid arguments passed to matrix functions""" 10 | 11 | 12 | class LightError(Exception): 13 | """Error raised when invalid arguments passed to light functions""" 14 | 15 | 16 | class MotorError(Exception): 17 | """Error raised when invalid arguments passed to motor functions""" 18 | 19 | 20 | class BuildHATError(Exception): 21 | """Error raised when HAT not found""" 22 | 23 | 24 | class DeviceError(Exception): 25 | """Error raised when there is a Device issue""" 26 | -------------------------------------------------------------------------------- /buildhat/force.py: -------------------------------------------------------------------------------- 1 | """Force sensor handling functionality""" 2 | 3 | from threading import Condition 4 | 5 | from .devices import Device 6 | 7 | 8 | class ForceSensor(Device): 9 | """Force sensor 10 | 11 | :param port: Port of device 12 | :raises DeviceError: Occurs if there is no force sensor attached to port 13 | """ 14 | 15 | def __init__(self, port, threshold_force=1): 16 | """Initialise force sensor 17 | 18 | :param port: Port of device 19 | :param threshold_force: Optional 20 | """ 21 | super().__init__(port) 22 | self.mode([(0, 0), (1, 0), (3, 0)]) 23 | self._when_pressed = None 24 | self._when_released = None 25 | self._fired_pressed = False 26 | self._fired_released = False 27 | self._cond_force = Condition() 28 | self._threshold_force = threshold_force 29 | 30 | def _intermediate(self, data): 31 | with self._cond_force: 32 | self._data = data[0] 33 | self._cond_force.notify() 34 | if data[0] >= self.threshold_force and not self._fired_pressed: 35 | if self._when_pressed is not None: 36 | self._when_pressed(data[0]) 37 | self._fired_pressed = True 38 | self._fired_released = False 39 | if data[0] < self.threshold_force and not self._fired_released: 40 | if self._when_released is not None: 41 | self._when_released(data[0]) 42 | self._fired_pressed = False 43 | self._fired_released = True 44 | 45 | @property 46 | def threshold_force(self): 47 | """Threshold force 48 | 49 | :getter: Returns threshold force 50 | :setter: Sets threshold force 51 | :return: Threshold force 52 | """ 53 | return self._threshold_force 54 | 55 | @threshold_force.setter 56 | def threshold_force(self, value): 57 | self._threshold_force = value 58 | 59 | def get_force(self): 60 | """Return the force in (N) 61 | 62 | :return: The force exerted on the button 63 | :rtype: int 64 | """ 65 | return self.get()[0] 66 | 67 | def get_peak_force(self): 68 | """Get the maximum force registered since the sensor was reset 69 | 70 | (The sensor gets reset when the firmware is reloaded) 71 | 72 | :return: 0 - 100 73 | :rtype: int 74 | """ 75 | return self.get()[2] 76 | 77 | def is_pressed(self): 78 | """Get whether the button is pressed 79 | 80 | :return: If button is pressed 81 | :rtype: bool 82 | """ 83 | return self.get()[1] == 1 84 | 85 | @property 86 | def when_pressed(self): 87 | """Handle force events 88 | 89 | :getter: Returns function to be called when pressed 90 | :setter: Sets function to be called when pressed 91 | :return: Callback function 92 | """ 93 | return self._when_pressed 94 | 95 | @when_pressed.setter 96 | def when_pressed(self, value): 97 | """Call back, when button is has pressed 98 | 99 | :param value: Callback function 100 | """ 101 | self._when_pressed = value 102 | self.callback(self._intermediate) 103 | 104 | @property 105 | def when_released(self): 106 | """Handle force events 107 | 108 | :getter: Returns function to be called when released 109 | :setter: Sets function to be called when released 110 | :return: Callback function 111 | """ 112 | return self._when_pressed 113 | 114 | @when_released.setter 115 | def when_released(self, value): 116 | """Call back, when button is has released 117 | 118 | :param value: Callback function 119 | """ 120 | self._when_released = value 121 | self.callback(self._intermediate) 122 | 123 | def wait_until_pressed(self, force=1): 124 | """Wait until the button is pressed 125 | 126 | :param force: Optional 127 | """ 128 | self.callback(self._intermediate) 129 | with self._cond_force: 130 | self._cond_force.wait() 131 | while self._data < force: 132 | self._cond_force.wait() 133 | 134 | def wait_until_released(self, force=0): 135 | """Wait until the button is released 136 | 137 | :param force: Optional 138 | """ 139 | self.callback(self._intermediate) 140 | with self._cond_force: 141 | self._cond_force.wait() 142 | while self._data > force: 143 | self._cond_force.wait() 144 | -------------------------------------------------------------------------------- /buildhat/hat.py: -------------------------------------------------------------------------------- 1 | """HAT handling functionality""" 2 | 3 | from concurrent.futures import Future 4 | 5 | from .devices import Device 6 | 7 | 8 | class Hat: 9 | """Allows enumeration of devices which are connected to the hat""" 10 | 11 | def __init__(self, device=None, debug=False): 12 | """Hat 13 | 14 | :param device: Optional string containing path to Build HAT serial device 15 | :param debug: Optional boolean to log debug information 16 | """ 17 | self.led_status = -1 18 | if device is None: 19 | Device._setup(debug=debug) 20 | else: 21 | Device._setup(device=device, debug=debug) 22 | 23 | def get(self): 24 | """Get devices which are connected or disconnected 25 | 26 | :return: Dictionary of devices 27 | :rtype: dict 28 | """ 29 | devices = {} 30 | for i in range(4): 31 | name = Device.UNKNOWN_DEVICE 32 | if Device._instance.connections[i].typeid in Device._device_names: 33 | name = Device._device_names[Device._instance.connections[i].typeid][0] 34 | desc = Device._device_names[Device._instance.connections[i].typeid][1] 35 | elif Device._instance.connections[i].typeid == -1: 36 | name = Device.DISCONNECTED_DEVICE 37 | desc = '' 38 | devices[chr(ord('A') + i)] = {"typeid": Device._instance.connections[i].typeid, 39 | "connected": Device._instance.connections[i].connected, 40 | "name": name, 41 | "description": desc} 42 | return devices 43 | 44 | def get_logfile(self): 45 | """Get the filename of the debug log (If enabled, None otherwise) 46 | 47 | :return: Path of the debug logfile 48 | :rtype: str or None 49 | """ 50 | return Device._instance.debug_filename 51 | 52 | def get_vin(self): 53 | """Get the voltage present on the input power jack 54 | 55 | :return: Voltage on the input power jack 56 | :rtype: float 57 | """ 58 | ftr = Future() 59 | Device._instance.vinftr.append(ftr) 60 | Device._instance.write(b"vin\r") 61 | return ftr.result() 62 | 63 | def _set_led(self, intmode): 64 | if isinstance(intmode, int) and intmode >= -1 and intmode <= 3: 65 | self.led_status = intmode 66 | Device._instance.write(f"ledmode {intmode}\r".encode()) 67 | 68 | def set_leds(self, color="voltage"): 69 | """Set the two LEDs on or off on the BuildHAT. 70 | 71 | By default the color depends on the input voltage with green being nominal at around 8V 72 | (The fastest time the LEDs can be perceptually toggled is around 0.025 seconds) 73 | 74 | :param color: orange, green, both, off, or voltage (default) 75 | """ 76 | if color == "orange": 77 | self._set_led(1) 78 | elif color == "green": 79 | self._set_led(2) 80 | elif color == "both": 81 | self._set_led(3) 82 | elif color == "off": 83 | self._set_led(0) 84 | elif color == "voltage": 85 | self._set_led(-1) 86 | else: 87 | return 88 | 89 | def orange_led(self, status=True): 90 | """Turn the BuildHAT's orange LED on or off 91 | 92 | :param status: True to turn it on, False to turn it off 93 | """ 94 | if status: 95 | if self.led_status == 3 or self.led_status == 1: 96 | # already on 97 | return 98 | elif self.led_status == 2: 99 | self._set_led(3) 100 | # off or default 101 | else: 102 | self._set_led(1) 103 | else: 104 | if self.led_status == 1 or self.led_status == -1: 105 | self._set_led(0) 106 | elif self.led_status == 3: 107 | self._set_led(2) 108 | 109 | def green_led(self, status=True): 110 | """Turn the BuildHAT's green LED on or off 111 | 112 | :param status: True to turn it on, False to turn it off 113 | """ 114 | if status: 115 | if self.led_status == 3 or self.led_status == 2: 116 | # already on 117 | return 118 | elif self.led_status == 1: 119 | self._set_led(3) 120 | # off or default 121 | else: 122 | self._set_led(2) 123 | else: 124 | if self.led_status == 2 or self.led_status == -1: 125 | self._set_led(0) 126 | elif self.led_status == 3: 127 | self._set_led(1) 128 | 129 | def _close(self): 130 | Device._instance.shutdown() 131 | -------------------------------------------------------------------------------- /buildhat/light.py: -------------------------------------------------------------------------------- 1 | """Light device handling functionality""" 2 | 3 | from .devices import Device 4 | from .exc import LightError 5 | 6 | 7 | class Light(Device): 8 | """Light 9 | 10 | Use on()/off() functions to turn lights on/off 11 | 12 | :param port: Port of device 13 | :raises DeviceError: Occurs if there is no light attached to port 14 | """ 15 | 16 | def __init__(self, port): 17 | """ 18 | Initialise light 19 | 20 | :param port: Port of device 21 | """ 22 | super().__init__(port) 23 | 24 | def brightness(self, brightness): 25 | """ 26 | Brightness of LEDs 27 | 28 | :param brightness: Brightness argument 0 to 100 29 | :raises LightError: Occurs if invalid brightness passed 30 | """ 31 | if not (brightness >= 0 and brightness <= 100): 32 | raise LightError("Need brightness arg, of 0 to 100") 33 | if brightness > 0: 34 | self._write(f"port {self.port} ; on ; set {brightness / 100.0}\r") 35 | else: 36 | self.off() 37 | 38 | def off(self): 39 | """Turn off lights""" 40 | # Using coast to turn off DIY lights completely 41 | self._write(f"port {self.port} ; coast\r") 42 | -------------------------------------------------------------------------------- /buildhat/matrix.py: -------------------------------------------------------------------------------- 1 | """Matrix device handling functionality""" 2 | 3 | from .devices import Device 4 | from .exc import MatrixError 5 | 6 | 7 | class Matrix(Device): 8 | """LED Matrix 9 | 10 | :param port: Port of device 11 | :raises DeviceError: Occurs if there is no LED matrix attached to port 12 | """ 13 | 14 | def __init__(self, port): 15 | """Initialise matrix 16 | 17 | :param port: Port of device 18 | """ 19 | super().__init__(port) 20 | self.on() 21 | self.mode(2) 22 | self._matrix = [[(0, 0) for x in range(3)] for y in range(3)] 23 | 24 | def set_pixels(self, matrix, display=True): 25 | """Write pixel data to LED matrix 26 | 27 | :param matrix: 3x3 list of tuples, with colour (0–10) and brightness (0–10) (see example for more detail) 28 | :param display: Whether to update matrix or not 29 | :raises MatrixError: Occurs if invalid matrix height/width provided 30 | """ 31 | if len(matrix) != 3: 32 | raise MatrixError("Incorrect matrix height") 33 | for x in range(3): 34 | if len(matrix[x]) != 3: 35 | raise MatrixError("Incorrect matrix width") 36 | for y in range(3): 37 | matrix[x][y] = Matrix.normalize_pixel(matrix[x][y]) # pylint: disable=too-many-function-args 38 | self._matrix = matrix 39 | if display: 40 | self._output() 41 | 42 | def _output(self): 43 | out = [0xc2] 44 | for x in range(3): 45 | for y in range(3): 46 | out.append((self._matrix[x][y][1] << 4) | self._matrix[x][y][0]) 47 | self.select() 48 | self._write1(out) 49 | self.deselect() 50 | 51 | @staticmethod 52 | def strtocolor(colorstr): 53 | """Return the BuldHAT's integer representation of a color string 54 | 55 | :param colorstr: str of a valid color 56 | :return: (0-10) representing the color 57 | :rtype: int 58 | :raises MatrixError: Occurs if invalid color specified 59 | """ 60 | if colorstr == "pink": 61 | return 1 62 | elif colorstr == "lilac": 63 | return 2 64 | elif colorstr == "blue": 65 | return 3 66 | elif colorstr == "cyan": 67 | return 4 68 | elif colorstr == "turquoise": 69 | return 5 70 | elif colorstr == "green": 71 | return 6 72 | elif colorstr == "yellow": 73 | return 7 74 | elif colorstr == "orange": 75 | return 8 76 | elif colorstr == "red": 77 | return 9 78 | elif colorstr == "white": 79 | return 10 80 | elif colorstr == "": 81 | return 0 82 | raise MatrixError("Invalid color specified") 83 | 84 | @staticmethod 85 | def normalize_pixel(pixel): 86 | """Validate a pixel tuple (color, brightness) and convert string colors to integers 87 | 88 | :param pixel: tuple of colour (0–10) or string (ie:"red") and brightness (0–10) 89 | :return: (color, brightness) integers 90 | :rtype: tuple 91 | :raises MatrixError: Occurs if invalid pixel specified 92 | """ 93 | if isinstance(pixel, tuple): 94 | c, brightness = pixel # pylint: disable=unpacking-non-sequence 95 | if isinstance(c, str): 96 | c = Matrix.strtocolor(c) # pylint: disable=too-many-function-args 97 | if not (isinstance(brightness, int) and isinstance(c, int)): 98 | raise MatrixError("Invalid pixel specified") 99 | if not (brightness >= 0 and brightness <= 10): 100 | raise MatrixError("Invalid brightness value specified") 101 | if not (c >= 0 and c <= 10): 102 | raise MatrixError("Invalid pixel color specified") 103 | return (c, brightness) 104 | else: 105 | raise MatrixError("Invalid pixel specified") 106 | 107 | @staticmethod 108 | def validate_coordinate(coord): 109 | """Validate an x,y coordinate for the 3x3 Matrix 110 | 111 | :param coord: tuple of 0-2 for the X coordinate and 0-2 for the Y coordinate 112 | :raises MatrixError: Occurs if invalid coordinate specified 113 | """ 114 | # pylint: disable=unsubscriptable-object 115 | if isinstance(coord, tuple): 116 | if not (isinstance(coord[0], int) and isinstance(coord[1], int)): 117 | raise MatrixError("Invalid coord specified") 118 | elif coord[0] > 2 or coord[0] < 0 or coord[1] > 2 or coord[1] < 0: 119 | raise MatrixError("Invalid coord specified") 120 | else: 121 | raise MatrixError("Invalid coord specified") 122 | 123 | def clear(self, pixel=None): 124 | """Clear matrix or set all as the same pixel 125 | 126 | :param pixel: tuple of colour (0–10) or string and brightness (0–10) 127 | """ 128 | if pixel is None: 129 | self._matrix = [[(0, 0) for x in range(3)] for y in range(3)] 130 | else: 131 | color = Matrix.normalize_pixel(pixel) # pylint: disable=too-many-function-args 132 | self._matrix = [[color for x in range(3)] for y in range(3)] 133 | self._output() 134 | 135 | def off(self): 136 | """Pretends to turn matrix off 137 | 138 | Never send the "off" command to the port a Matrix is connected to 139 | Instead, just turn all the pixels off 140 | """ 141 | self.clear() 142 | 143 | def level(self, level): 144 | """Use the matrix as a "level" meter from 0-9 145 | 146 | (The level meter is expressed in green which seems to be unchangeable) 147 | 148 | :param level: The height of the bar graph, 0-9 149 | :raises MatrixError: Occurs if invalid level specified 150 | """ 151 | if not isinstance(level, int): 152 | raise MatrixError("Invalid level, not integer") 153 | if not (level >= 0 and level <= 9): 154 | raise MatrixError("Invalid level specified") 155 | self.mode(0) 156 | self.select() 157 | self._write1([0xc0, level]) 158 | self.mode(2) # The rest of the Matrix code seems to expect this to be always set 159 | self.deselect() 160 | 161 | def set_transition(self, transition): 162 | """Set the transition mode between pixels 163 | 164 | Use display=False on set_pixel() or use set_pixels() to achieve desired 165 | results with transitions. 166 | 167 | Setting a new transition mode will wipe the screen and interrupt any 168 | running transition. 169 | 170 | Mode 0: No transition, immediate pixel drawing 171 | 172 | Mode 1: Right-to-left wipe in/out 173 | 174 | If the timing between writing new matrix pixels is less than one second 175 | the transition will clip columns of pixels from the right. 176 | 177 | Mode 2: Fade-in/Fade-out 178 | 179 | The fade in and fade out take about 2.2 seconds for full fade effect. 180 | Waiting less time between setting new pixels will result in a faster 181 | fade which will cause the fade to "pop" in brightness. 182 | 183 | :param transition: Transition mode (0-2) 184 | :raises MatrixError: Occurs if invalid transition 185 | """ 186 | if not isinstance(transition, int): 187 | raise MatrixError("Invalid transition, not integer") 188 | if not (transition >= 0 and transition <= 2): 189 | raise MatrixError("Invalid transition specified") 190 | self.mode(3) 191 | self.select() 192 | self._write1([0xc3, transition]) 193 | self.mode(2) # The rest of the Matrix code seems to expect this to be always set 194 | self.deselect() 195 | 196 | def set_pixel(self, coord, pixel, display=True): 197 | """Write pixel to coordinate 198 | 199 | :param coord: (0,0) to (2,2) 200 | :param pixel: tuple of colour (0–10) or string and brightness (0–10) 201 | :param display: Whether to update matrix or not 202 | """ 203 | color = Matrix.normalize_pixel(pixel) # pylint: disable=too-many-function-args 204 | Matrix.validate_coordinate(coord) # pylint: disable=too-many-function-args 205 | x, y = coord 206 | self._matrix[x][y] = color 207 | if display: 208 | self._output() 209 | -------------------------------------------------------------------------------- /buildhat/motors.py: -------------------------------------------------------------------------------- 1 | """Motor device handling functionality""" 2 | 3 | import threading 4 | import time 5 | from collections import deque 6 | from concurrent.futures import Future 7 | from enum import Enum 8 | from threading import Condition 9 | 10 | from .devices import Device 11 | from .exc import MotorError 12 | 13 | 14 | class PassiveMotor(Device): 15 | """Passive Motor device 16 | 17 | :param port: Port of device 18 | :raises DeviceError: Occurs if there is no passive motor attached to port 19 | """ 20 | 21 | def __init__(self, port): 22 | """Initialise motor 23 | 24 | :param port: Port of device 25 | """ 26 | super().__init__(port) 27 | self._default_speed = 20 28 | self._currentspeed = 0 29 | self.plimit(0.7) 30 | 31 | def set_default_speed(self, default_speed): 32 | """Set the default speed of the motor 33 | 34 | :param default_speed: Speed ranging from -100 to 100 35 | :raises MotorError: Occurs if invalid speed passed 36 | """ 37 | if not (default_speed >= -100 and default_speed <= 100): 38 | raise MotorError("Invalid Speed") 39 | self._default_speed = default_speed 40 | 41 | def start(self, speed=None): 42 | """Start motor 43 | 44 | :param speed: Speed ranging from -100 to 100 45 | :raises MotorError: Occurs if invalid speed passed 46 | """ 47 | if self._currentspeed == speed: 48 | # Already running at this speed, do nothing 49 | return 50 | 51 | if speed is None: 52 | speed = self._default_speed 53 | else: 54 | if not (speed >= -100 and speed <= 100): 55 | raise MotorError("Invalid Speed") 56 | self._currentspeed = speed 57 | cmd = f"port {self.port} ; pwm ; set {speed / 100}\r" 58 | self._write(cmd) 59 | 60 | def stop(self): 61 | """Stop motor""" 62 | cmd = f"port {self.port} ; off\r" 63 | self._write(cmd) 64 | self._currentspeed = 0 65 | 66 | def plimit(self, plimit): 67 | """Limit power 68 | 69 | :param plimit: Value 0 to 1 70 | :raises MotorError: Occurs if invalid plimit value passed 71 | """ 72 | if not (plimit >= 0 and plimit <= 1): 73 | raise MotorError("plimit should be 0 to 1") 74 | self._write(f"port {self.port} ; port_plimit {plimit}\r") 75 | 76 | def bias(self, bias): 77 | """Bias motor 78 | 79 | :param bias: Value 0 to 1 80 | :raises MotorError: Occurs if invalid bias value passed 81 | 82 | .. deprecated:: 0.6.0 83 | """ # noqa: RST303 84 | raise MotorError("Bias no longer available") 85 | 86 | 87 | class MotorRunmode(Enum): 88 | """Current mode motor is in""" 89 | 90 | NONE = 0 91 | FREE = 1 92 | DEGREES = 2 93 | SECONDS = 3 94 | 95 | 96 | class Motor(Device): 97 | """Motor device 98 | 99 | :param port: Port of device 100 | :raises DeviceError: Occurs if there is no motor attached to port 101 | """ 102 | 103 | def __init__(self, port): 104 | """Initialise motor 105 | 106 | :param port: Port of device 107 | """ 108 | super().__init__(port) 109 | self.default_speed = 20 110 | self._currentspeed = 0 111 | if self._typeid in {38}: 112 | self.mode([(1, 0), (2, 0)]) 113 | self._combi = "1 0 2 0" 114 | self._noapos = True 115 | else: 116 | self.mode([(1, 0), (2, 0), (3, 0)]) 117 | self._combi = "1 0 2 0 3 0" 118 | self._noapos = False 119 | self.plimit(0.7) 120 | self.pwmparams(0.65, 0.01) 121 | self._rpm = False 122 | self._release = True 123 | self._bqueue = deque(maxlen=5) 124 | self._cvqueue = Condition() 125 | self.when_rotated = None 126 | self._oldpos = None 127 | self._runmode = MotorRunmode.NONE 128 | 129 | def set_speed_unit_rpm(self, rpm=False): 130 | """Set whether to use RPM for speed units or not 131 | 132 | :param rpm: Boolean to determine whether to use RPM for units 133 | """ 134 | self._rpm = rpm 135 | 136 | def set_default_speed(self, default_speed): 137 | """Set the default speed of the motor 138 | 139 | :param default_speed: Speed ranging from -100 to 100 140 | :raises MotorError: Occurs if invalid speed passed 141 | """ 142 | if not (default_speed >= -100 and default_speed <= 100): 143 | raise MotorError("Invalid Speed") 144 | self.default_speed = default_speed 145 | 146 | def run_for_rotations(self, rotations, speed=None, blocking=True): 147 | """Run motor for N rotations 148 | 149 | :param rotations: Number of rotations 150 | :param speed: Speed ranging from -100 to 100 151 | :param blocking: Whether call should block till finished 152 | :raises MotorError: Occurs if invalid speed passed 153 | """ 154 | self._runmode = MotorRunmode.DEGREES 155 | if speed is None: 156 | self.run_for_degrees(int(rotations * 360), self.default_speed, blocking) 157 | else: 158 | if not (speed >= -100 and speed <= 100): 159 | raise MotorError("Invalid Speed") 160 | self.run_for_degrees(int(rotations * 360), speed, blocking) 161 | 162 | def _run_for_degrees(self, degrees, speed): 163 | self._runmode = MotorRunmode.DEGREES 164 | mul = 1 165 | if speed < 0: 166 | speed = abs(speed) 167 | mul = -1 168 | pos = self.get_position() 169 | newpos = ((degrees * mul) + pos) / 360.0 170 | pos /= 360.0 171 | self._run_positional_ramp(pos, newpos, speed) 172 | self._runmode = MotorRunmode.NONE 173 | 174 | def _run_to_position(self, degrees, speed, direction): 175 | self._runmode = MotorRunmode.DEGREES 176 | data = self.get() 177 | pos = data[1] 178 | if self._noapos: 179 | apos = pos 180 | else: 181 | apos = data[2] 182 | diff = (degrees - apos + 180) % 360 - 180 183 | newpos = (pos + diff) / 360 184 | v1 = (degrees - apos) % 360 185 | v2 = (apos - degrees) % 360 186 | mul = 1 187 | if diff > 0: 188 | mul = -1 189 | diff = sorted([diff, mul * (v2 if abs(diff) == v1 else v1)]) 190 | if direction == "shortest": 191 | pass 192 | elif direction == "clockwise": 193 | newpos = (pos + diff[1]) / 360 194 | elif direction == "anticlockwise": 195 | newpos = (pos + diff[0]) / 360 196 | else: 197 | raise MotorError("Invalid direction, should be: shortest, clockwise or anticlockwise") 198 | # Convert current motor position to decimal rotations from preset position to match newpos units 199 | pos /= 360.0 200 | self._run_positional_ramp(pos, newpos, speed) 201 | self._runmode = MotorRunmode.NONE 202 | 203 | def _run_positional_ramp(self, pos, newpos, speed): 204 | """Ramp motor 205 | 206 | :param pos: Current motor position in decimal rotations (from preset position) 207 | :param newpos: New motor postion in decimal rotations (from preset position) 208 | :param speed: -100 to 100 209 | """ 210 | if self._rpm: 211 | speed = self._speed_process(speed) 212 | else: 213 | speed *= 0.05 # Collapse speed range to -5 to 5 214 | dur = abs((newpos - pos) / speed) 215 | cmd = (f"port {self.port}; select 0 ; selrate {self._interval}; " 216 | f"pid {self.port} 0 1 s4 0.0027777778 0 5 0 .1 3 0.01; " 217 | f"set ramp {pos} {newpos} {dur} 0\r") 218 | ftr = Future() 219 | self._hat.rampftr[self.port].append(ftr) 220 | self._write(cmd) 221 | ftr.result() 222 | if self._release: 223 | time.sleep(0.2) 224 | self.coast() 225 | 226 | def run_for_degrees(self, degrees, speed=None, blocking=True): 227 | """Run motor for N degrees 228 | 229 | Speed of 1 means 1 revolution / second 230 | 231 | :param degrees: Number of degrees to rotate 232 | :param speed: Speed ranging from -100 to 100 233 | :param blocking: Whether call should block till finished 234 | :raises MotorError: Occurs if invalid speed passed 235 | """ 236 | self._runmode = MotorRunmode.DEGREES 237 | if speed is None: 238 | speed = self.default_speed 239 | if not (speed >= -100 and speed <= 100): 240 | raise MotorError("Invalid Speed") 241 | if not blocking: 242 | self._queue((self._run_for_degrees, (degrees, speed))) 243 | else: 244 | self._wait_for_nonblocking() 245 | self._run_for_degrees(degrees, speed) 246 | 247 | def run_to_position(self, degrees, speed=None, blocking=True, direction="shortest"): 248 | """Run motor to position (in degrees) 249 | 250 | :param degrees: Position in degrees from -180 to 180 251 | :param speed: Speed ranging from 0 to 100 252 | :param blocking: Whether call should block till finished 253 | :param direction: shortest (default)/clockwise/anticlockwise 254 | :raises MotorError: Occurs if invalid speed or angle passed 255 | """ 256 | self._runmode = MotorRunmode.DEGREES 257 | if speed is None: 258 | speed = self.default_speed 259 | if not (speed >= 0 and speed <= 100): 260 | raise MotorError("Invalid Speed") 261 | if degrees < -180 or degrees > 180: 262 | raise MotorError("Invalid angle") 263 | if not blocking: 264 | self._queue((self._run_to_position, (degrees, speed, direction))) 265 | else: 266 | self._wait_for_nonblocking() 267 | self._run_to_position(degrees, speed, direction) 268 | 269 | def _run_for_seconds(self, seconds, speed): 270 | speed = self._speed_process(speed) 271 | self._runmode = MotorRunmode.SECONDS 272 | if self._rpm: 273 | pid = f"pid_diff {self.port} 0 5 s2 0.0027777778 1 0 2.5 0 .4 0.01; " 274 | else: 275 | pid = f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100 0.01;" 276 | cmd = (f"port {self.port} ; select 0 ; selrate {self._interval}; " 277 | f"{pid}" 278 | f"set pulse {speed} 0.0 {seconds} 0\r") 279 | ftr = Future() 280 | self._hat.pulseftr[self.port].append(ftr) 281 | self._write(cmd) 282 | ftr.result() 283 | if self._release: 284 | self.coast() 285 | self._runmode = MotorRunmode.NONE 286 | 287 | def run_for_seconds(self, seconds, speed=None, blocking=True): 288 | """Run motor for N seconds 289 | 290 | :param seconds: Time in seconds 291 | :param speed: Speed ranging from -100 to 100 292 | :param blocking: Whether call should block till finished 293 | :raises MotorError: Occurs when invalid speed specified 294 | """ 295 | self._runmode = MotorRunmode.SECONDS 296 | if speed is None: 297 | speed = self.default_speed 298 | if not (speed >= -100 and speed <= 100): 299 | raise MotorError("Invalid Speed") 300 | if not blocking: 301 | self._queue((self._run_for_seconds, (seconds, speed))) 302 | else: 303 | self._wait_for_nonblocking() 304 | self._run_for_seconds(seconds, speed) 305 | 306 | def start(self, speed=None): 307 | """Start motor 308 | 309 | :param speed: Speed ranging from -100 to 100 310 | :raises MotorError: Occurs when invalid speed specified 311 | """ 312 | self._wait_for_nonblocking() 313 | if self._runmode == MotorRunmode.FREE: 314 | if self._currentspeed == speed: 315 | # Already running at this speed, do nothing 316 | return 317 | elif self._runmode != MotorRunmode.NONE: 318 | # Motor is running some other mode, wait for it to stop or stop() it yourself 319 | return 320 | 321 | if speed is None: 322 | speed = self.default_speed 323 | else: 324 | if not (speed >= -100 and speed <= 100): 325 | raise MotorError("Invalid Speed") 326 | speed = self._speed_process(speed) 327 | cmd = f"port {self.port} ; set {speed}\r" 328 | if self._runmode == MotorRunmode.NONE: 329 | if self._rpm: 330 | pid = f"pid_diff {self.port} 0 5 s2 0.0027777778 1 0 2.5 0 .4 0.01; " 331 | else: 332 | pid = f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100 0.01; " 333 | cmd = (f"port {self.port} ; select 0 ; selrate {self._interval}; " 334 | f"{pid}" 335 | f"set {speed}\r") 336 | self._runmode = MotorRunmode.FREE 337 | self._currentspeed = speed 338 | self._write(cmd) 339 | 340 | def stop(self): 341 | """Stop motor""" 342 | self._wait_for_nonblocking() 343 | self._runmode = MotorRunmode.NONE 344 | self._currentspeed = 0 345 | self.coast() 346 | 347 | def get_position(self): 348 | """Get position of motor with relation to preset position (can be negative or positive) 349 | 350 | :return: Position of motor in degrees from preset position 351 | :rtype: int 352 | """ 353 | return self.get()[1] 354 | 355 | def get_aposition(self): 356 | """Get absolute position of motor 357 | 358 | :return: Absolute position of motor from -180 to 180 359 | :rtype: int 360 | """ 361 | if self._noapos: 362 | raise MotorError("No absolute position with this motor") 363 | else: 364 | return self.get()[2] 365 | 366 | def get_speed(self): 367 | """Get speed of motor 368 | 369 | :return: Speed of motor 370 | :rtype: int 371 | """ 372 | return self.get()[0] 373 | 374 | @property 375 | def when_rotated(self): 376 | """ 377 | Handle rotation events 378 | 379 | :getter: Returns function to be called when rotated 380 | :setter: Sets function to be called when rotated 381 | :return: Callback function 382 | """ 383 | return self._when_rotated 384 | 385 | def _intermediate(self, data): 386 | if self._noapos: 387 | speed, pos = data 388 | apos = None 389 | else: 390 | speed, pos, apos = data 391 | if self._oldpos is None: 392 | self._oldpos = pos 393 | return 394 | if abs(pos - self._oldpos) >= 1: 395 | if self._when_rotated is not None: 396 | self._when_rotated(speed, pos, apos) 397 | self._oldpos = pos 398 | 399 | @when_rotated.setter 400 | def when_rotated(self, value): 401 | """Call back, when motor has been rotated 402 | 403 | :param value: Callback function 404 | """ 405 | self._when_rotated = value 406 | self.callback(self._intermediate) 407 | 408 | def plimit(self, plimit): 409 | """Limit power 410 | 411 | :param plimit: Value 0 to 1 412 | :raises MotorError: Occurs if invalid plimit value passed 413 | """ 414 | if not (plimit >= 0 and plimit <= 1): 415 | raise MotorError("plimit should be 0 to 1") 416 | self._write(f"port {self.port} ; port_plimit {plimit}\r") 417 | 418 | def bias(self, bias): 419 | """Bias motor 420 | 421 | :param bias: Value 0 to 1 422 | :raises MotorError: Occurs if invalid bias value passed 423 | 424 | .. deprecated:: 0.6.0 425 | """ # noqa: RST303 426 | raise MotorError("Bias no longer available") 427 | 428 | def pwmparams(self, pwmthresh, minpwm): 429 | """PWM thresholds 430 | 431 | :param pwmthresh: Value 0 to 1, threshold below, will switch from fast to slow, PWM 432 | :param minpwm: Value 0 to 1, threshold below which it switches off the drive altogether 433 | :raises MotorError: Occurs if invalid values are passed 434 | """ 435 | if not (pwmthresh >= 0 and pwmthresh <= 1): 436 | raise MotorError("pwmthresh should be 0 to 1") 437 | if not (minpwm >= 0 and minpwm <= 1): 438 | raise MotorError("minpwm should be 0 to 1") 439 | self._write(f"port {self.port} ; pwmparams {pwmthresh} {minpwm}\r") 440 | 441 | def pwm(self, pwmv): 442 | """PWM motor 443 | 444 | :param pwmv: Value -1 to 1 445 | :raises MotorError: Occurs if invalid pwm value passed 446 | """ 447 | if not (pwmv >= -1 and pwmv <= 1): 448 | raise MotorError("pwm should be -1 to 1") 449 | self._write(f"port {self.port} ; pwm ; set {pwmv}\r") 450 | 451 | def coast(self): 452 | """Coast motor""" 453 | self._write(f"port {self.port} ; coast\r") 454 | 455 | def float(self): 456 | """Float motor""" 457 | self.pwm(0) 458 | 459 | @property 460 | def release(self): 461 | """Determine if motor is released after running, so can be turned by hand 462 | 463 | :getter: Returns whether motor is released, so can be turned by hand 464 | :setter: Sets whether motor is released, so can be turned by hand 465 | :return: Whether motor is released, so can be turned by hand 466 | :rtype: bool 467 | """ 468 | return self._release 469 | 470 | @release.setter 471 | def release(self, value): 472 | """Determine if the motor is released after running, so can be turned by hand 473 | 474 | :param value: Whether motor should be released, so can be turned by hand 475 | :type value: bool 476 | """ 477 | if not isinstance(value, bool): 478 | raise MotorError("Must pass boolean") 479 | self._release = value 480 | 481 | def _queue(self, cmd): 482 | Device._instance.motorqueue[self.port].put(cmd) 483 | 484 | def _wait_for_nonblocking(self): 485 | """Wait for nonblocking commands to finish""" 486 | Device._instance.motorqueue[self.port].join() 487 | 488 | def _speed_process(self, speed): 489 | """Lower speed value""" 490 | if self._rpm: 491 | return speed / 60 492 | else: 493 | return speed 494 | 495 | 496 | class MotorPair: 497 | """Pair of motors 498 | 499 | :param motora: One of the motors to drive 500 | :param motorb: Other motor in pair to drive 501 | :raises DeviceError: Occurs if there is no motor attached to port 502 | """ 503 | 504 | def __init__(self, leftport, rightport): 505 | """Initialise pair of motors 506 | 507 | :param leftport: Left motor port 508 | :param rightport: Right motor port 509 | """ 510 | super().__init__() 511 | self._leftmotor = Motor(leftport) 512 | self._rightmotor = Motor(rightport) 513 | self.default_speed = 20 514 | self._release = True 515 | self._rpm = False 516 | 517 | def set_default_speed(self, default_speed): 518 | """Set the default speed of the motor 519 | 520 | :param default_speed: Speed ranging from -100 to 100 521 | """ 522 | self.default_speed = default_speed 523 | 524 | def set_speed_unit_rpm(self, rpm=False): 525 | """Set whether to use RPM for speed units or not 526 | 527 | :param rpm: Boolean to determine whether to use RPM for units 528 | """ 529 | self._rpm = rpm 530 | self._leftmotor.set_speed_unit_rpm(rpm) 531 | self._rightmotor.set_speed_unit_rpm(rpm) 532 | 533 | def run_for_rotations(self, rotations, speedl=None, speedr=None): 534 | """Run pair of motors for N rotations 535 | 536 | :param rotations: Number of rotations 537 | :param speedl: Speed ranging from -100 to 100 538 | :param speedr: Speed ranging from -100 to 100 539 | """ 540 | if speedl is None: 541 | speedl = self.default_speed 542 | if speedr is None: 543 | speedr = self.default_speed 544 | self.run_for_degrees(int(rotations * 360), speedl, speedr) 545 | 546 | def run_for_degrees(self, degrees, speedl=None, speedr=None): 547 | """Run pair of motors for degrees 548 | 549 | :param degrees: Number of degrees 550 | :param speedl: Speed ranging from -100 to 100 551 | :param speedr: Speed ranging from -100 to 100 552 | """ 553 | if speedl is None: 554 | speedl = self.default_speed 555 | if speedr is None: 556 | speedr = self.default_speed 557 | th1 = threading.Thread(target=self._leftmotor._run_for_degrees, args=(degrees, speedl)) 558 | th2 = threading.Thread(target=self._rightmotor._run_for_degrees, args=(degrees, speedr)) 559 | th1.daemon = True 560 | th2.daemon = True 561 | th1.start() 562 | th2.start() 563 | th1.join() 564 | th2.join() 565 | 566 | def run_for_seconds(self, seconds, speedl=None, speedr=None): 567 | """Run pair for N seconds 568 | 569 | :param seconds: Time in seconds 570 | :param speedl: Speed ranging from -100 to 100 571 | :param speedr: Speed ranging from -100 to 100 572 | """ 573 | if speedl is None: 574 | speedl = self.default_speed 575 | if speedr is None: 576 | speedr = self.default_speed 577 | th1 = threading.Thread(target=self._leftmotor._run_for_seconds, args=(seconds, speedl)) 578 | th2 = threading.Thread(target=self._rightmotor._run_for_seconds, args=(seconds, speedr)) 579 | th1.daemon = True 580 | th2.daemon = True 581 | th1.start() 582 | th2.start() 583 | th1.join() 584 | th2.join() 585 | 586 | def start(self, speedl=None, speedr=None): 587 | """Start motors 588 | 589 | :param speedl: Speed ranging from -100 to 100 590 | :param speedr: Speed ranging from -100 to 100 591 | """ 592 | if speedl is None: 593 | speedl = self.default_speed 594 | if speedr is None: 595 | speedr = self.default_speed 596 | self._leftmotor.start(speedl) 597 | self._rightmotor.start(speedr) 598 | 599 | def stop(self): 600 | """Stop motors""" 601 | self._leftmotor.stop() 602 | self._rightmotor.stop() 603 | 604 | def run_to_position(self, degreesl, degreesr, speed=None, direction="shortest"): 605 | """Run pair to position (in degrees) 606 | 607 | :param degreesl: Position in degrees for left motor 608 | :param degreesr: Position in degrees for right motor 609 | :param speed: Speed ranging from -100 to 100 610 | :param direction: shortest (default)/clockwise/anticlockwise 611 | """ 612 | if speed is None: 613 | th1 = threading.Thread(target=self._leftmotor._run_to_position, args=(degreesl, self.default_speed, direction)) 614 | th2 = threading.Thread(target=self._rightmotor._run_to_position, args=(degreesr, self.default_speed, direction)) 615 | else: 616 | th1 = threading.Thread(target=self._leftmotor._run_to_position, args=(degreesl, speed, direction)) 617 | th2 = threading.Thread(target=self._rightmotor._run_to_position, args=(degreesr, speed, direction)) 618 | th1.daemon = True 619 | th2.daemon = True 620 | th1.start() 621 | th2.start() 622 | th1.join() 623 | th2.join() 624 | 625 | @property 626 | def release(self): 627 | """Determine if motors are released after running, so can be turned by hand 628 | 629 | :getter: Returns whether motors are released, so can be turned by hand 630 | :setter: Sets whether motors are released, so can be turned by hand 631 | :return: Whether motors are released, so can be turned by hand 632 | :rtype: bool 633 | """ 634 | return self._release 635 | 636 | @release.setter 637 | def release(self, value): 638 | """Determine if motors are released after running, so can be turned by hand 639 | 640 | :param value: Whether motors should be released, so can be turned by hand 641 | :type value: bool 642 | """ 643 | if not isinstance(value, bool): 644 | raise MotorError("Must pass boolean") 645 | self._release = value 646 | self._leftmotor.release = value 647 | self._rightmotor.release = value 648 | -------------------------------------------------------------------------------- /buildhat/serinterface.py: -------------------------------------------------------------------------------- 1 | """Build HAT handling functionality""" 2 | 3 | import logging 4 | import os 5 | import queue 6 | import tempfile 7 | import threading 8 | import time 9 | from enum import Enum 10 | from threading import Condition, Timer 11 | 12 | import serial 13 | from gpiozero import DigitalOutputDevice 14 | 15 | from .exc import BuildHATError 16 | 17 | 18 | class HatState(Enum): 19 | """Current state that hat is in""" 20 | 21 | OTHER = 0 22 | FIRMWARE = 1 23 | NEEDNEWFIRMWARE = 2 24 | BOOTLOADER = 3 25 | 26 | 27 | class Connection: 28 | """Connection information for a port""" 29 | 30 | def __init__(self): 31 | """Initialise connection""" 32 | self.typeid = -1 33 | self.connected = False 34 | self.callit = None 35 | self.simplemode = -1 36 | self.combimode = -1 37 | 38 | def update(self, typeid, connected, callit=None): 39 | """Update connection information for port 40 | 41 | :param typeid: Type ID of device on port 42 | :param connected: Whether device is connected or not 43 | :param callit: Callback function 44 | """ 45 | self.typeid = typeid 46 | self.connected = connected 47 | self.callit = callit 48 | 49 | 50 | def cmp(str1, str2): 51 | """Look for str2 in str1 52 | 53 | :param str1: String to look in 54 | :param str2: String to look for 55 | :return: Whether str2 exists 56 | """ 57 | return str1[:len(str2)] == str2 58 | 59 | 60 | class BuildHAT: 61 | """Interacts with Build HAT via UART interface""" 62 | 63 | CONNECTED = ": connected to active ID" 64 | CONNECTEDPASSIVE = ": connected to passive ID" 65 | DISCONNECTED = ": disconnected" 66 | DEVTIMEOUT = ": timeout during data phase: disconnecting" 67 | NOTCONNECTED = ": no device detected" 68 | PULSEDONE = ": pulse done" 69 | RAMPDONE = ": ramp done" 70 | FIRMWARE = "Firmware version: " 71 | BOOTLOADER = "BuildHAT bootloader version" 72 | DONE = "Done initialising ports" 73 | PROMPT = "BHBL>" 74 | RESET_GPIO_NUMBER = 4 75 | BOOT0_GPIO_NUMBER = 22 76 | 77 | def __init__(self, firmware, signature, version, device="/dev/serial0", debug=False): 78 | """Interact with Build HAT 79 | 80 | :param firmware: Firmware file 81 | :param signature: Signature file 82 | :param version: Firmware version 83 | :param device: Serial device to use 84 | :param debug: Optional boolean to log debug information 85 | :raises BuildHATError: Occurs if can't find HAT 86 | """ 87 | self.cond = Condition() 88 | self.state = HatState.OTHER 89 | self.connections = [] 90 | self.portftr = [] 91 | self.pulseftr = [] 92 | self.rampftr = [] 93 | self.vinftr = [] 94 | self.motorqueue = [] 95 | self.fin = False 96 | self.running = True 97 | self.debug_filename = None 98 | if debug: 99 | tmp = tempfile.NamedTemporaryFile(suffix=".log", prefix="buildhat-", delete=False) 100 | self.debug_filename = tmp.name 101 | logging.basicConfig(filename=tmp.name, format='%(asctime)s %(message)s', 102 | level=logging.DEBUG) 103 | 104 | for _ in range(4): 105 | self.connections.append(Connection()) 106 | self.portftr.append([]) 107 | self.pulseftr.append([]) 108 | self.rampftr.append([]) 109 | self.motorqueue.append(queue.Queue()) 110 | 111 | # On a Pi 5 /dev/serial0 will point to /dev/ttyAMA10 (which *only* 112 | # exists on a Pi 5, and is the 3-pin debug UART connector) 113 | # The UART on the Pi 5 GPIO header is /dev/ttyAMA0 114 | if device == "/dev/serial0" and os.readlink(device) == "ttyAMA10": 115 | device = "/dev/ttyAMA0" 116 | self.ser = serial.Serial(device, 115200, timeout=5) 117 | # Check if we're in the bootloader or the firmware 118 | self.write(b"version\r") 119 | 120 | emptydata = 0 121 | incdata = 0 122 | while True: 123 | line = self.read() 124 | if len(line) == 0: 125 | # Didn't receive any data 126 | emptydata += 1 127 | if emptydata > 3: 128 | break 129 | else: 130 | continue 131 | if cmp(line, BuildHAT.FIRMWARE): 132 | self.state = HatState.FIRMWARE 133 | ver = line[len(BuildHAT.FIRMWARE):].split(' ') 134 | if int(ver[0]) == version: 135 | self.state = HatState.FIRMWARE 136 | break 137 | else: 138 | self.state = HatState.NEEDNEWFIRMWARE 139 | break 140 | elif cmp(line, BuildHAT.BOOTLOADER): 141 | self.state = HatState.BOOTLOADER 142 | break 143 | else: 144 | # got other data we didn't understand - send version again 145 | incdata += 1 146 | if incdata > 5: 147 | break 148 | else: 149 | self.write(b"version\r") 150 | 151 | if self.state == HatState.NEEDNEWFIRMWARE: 152 | self.resethat() 153 | self.loadfirmware(firmware, signature) 154 | elif self.state == HatState.BOOTLOADER: 155 | self.loadfirmware(firmware, signature) 156 | elif self.state == HatState.OTHER: 157 | raise BuildHATError("HAT not found") 158 | 159 | self.cbqueue = queue.Queue() 160 | self.cb = threading.Thread(target=self.callbackloop, args=(self.cbqueue,)) 161 | self.cb.daemon = True 162 | self.cb.start() 163 | 164 | for q in self.motorqueue: 165 | ml = threading.Thread(target=self.motorloop, args=(q,)) 166 | ml.daemon = True 167 | ml.start() 168 | 169 | # Drop timeout value to 1s 170 | listevt = threading.Event() 171 | self.ser.timeout = 1 172 | self.th = threading.Thread(target=self.loop, args=(self.cond, self.state == HatState.FIRMWARE, self.cbqueue, listevt)) 173 | self.th.daemon = True 174 | self.th.start() 175 | 176 | if self.state == HatState.FIRMWARE: 177 | self.write(b"port 0 ; select ; port 1 ; select ; port 2 ; select ; port 3 ; select ; echo 0\r") 178 | self.write(b"list\r") 179 | listevt.set() 180 | elif self.state == HatState.NEEDNEWFIRMWARE or self.state == HatState.BOOTLOADER: 181 | self.write(b"reboot\r") 182 | 183 | # wait for initialisation to finish 184 | with self.cond: 185 | self.cond.wait() 186 | 187 | def resethat(self): 188 | """Reset the HAT""" 189 | reset = DigitalOutputDevice(BuildHAT.RESET_GPIO_NUMBER) 190 | boot0 = DigitalOutputDevice(BuildHAT.BOOT0_GPIO_NUMBER) 191 | boot0.off() 192 | reset.off() 193 | time.sleep(0.01) 194 | reset.on() 195 | time.sleep(0.01) 196 | boot0.close() 197 | reset.close() 198 | time.sleep(0.5) 199 | 200 | def loadfirmware(self, firmware, signature): 201 | """Load firmware 202 | 203 | :param firmware: Firmware to load 204 | :param signature: Signature to load 205 | """ 206 | with open(firmware, "rb") as f: 207 | firm = f.read() 208 | with open(signature, "rb") as f: 209 | sig = f.read() 210 | self.write(b"clear\r") 211 | self.getprompt() 212 | self.write(f"load {len(firm)} {self.checksum(firm)}\r".encode()) 213 | time.sleep(0.1) 214 | self.write(b"\x02", replace="0x02") 215 | self.write(firm, replace="--firmware file--") 216 | self.write(b"\x03\r", replace="0x03") 217 | self.getprompt() 218 | self.write(f"signature {len(sig)}\r".encode()) 219 | time.sleep(0.1) 220 | self.write(b"\x02", replace="0x02") 221 | self.write(sig, replace="--signature file--") 222 | self.write(b"\x03\r", replace="0x03") 223 | self.getprompt() 224 | 225 | def getprompt(self): 226 | """Loop until prompt is found 227 | 228 | Need to decide what we will do, when no prompt 229 | """ 230 | while True: 231 | line = self.read() 232 | if cmp(line, BuildHAT.PROMPT): 233 | break 234 | 235 | def checksum(self, data): 236 | """Calculate checksum from data 237 | 238 | :param data: Data to calculate the checksum from 239 | :return: Checksum that has been calculated 240 | """ 241 | u = 1 242 | for i in range(0, len(data)): 243 | if (u & 0x80000000) != 0: 244 | u = (u << 1) ^ 0x1d872b41 245 | else: 246 | u = u << 1 247 | u = (u ^ data[i]) & 0xFFFFFFFF 248 | return u 249 | 250 | def write(self, data, log=True, replace=""): 251 | """Write data to the serial port of Build HAT 252 | 253 | :param data: Data to write to Build HAT 254 | :param log: Whether to log line or not 255 | :param replace: Whether to log an alternative string 256 | """ 257 | self.ser.write(data) 258 | if not self.fin and log: 259 | if replace != "": 260 | logging.debug(f"> {replace}") 261 | else: 262 | logging.debug(f"> {data.decode('utf-8', 'ignore').strip()}") 263 | 264 | def read(self): 265 | """Read data from the serial port of Build HAT 266 | 267 | :return: Line that has been read 268 | """ 269 | line = "" 270 | try: 271 | line = self.ser.readline().decode('utf-8', 'ignore').strip() 272 | except serial.SerialException: 273 | pass 274 | if line != "": 275 | logging.debug(f"< {line}") 276 | return line 277 | 278 | def shutdown(self): 279 | """Turn off the Build HAT devices""" 280 | if not self.fin: 281 | self.fin = True 282 | self.running = False 283 | self.th.join() 284 | self.cbqueue.put(()) 285 | for q in self.motorqueue: 286 | q.put((None, None)) 287 | self.cb.join() 288 | turnoff = "" 289 | for p in range(4): 290 | conn = self.connections[p] 291 | if conn.typeid != 64: 292 | turnoff += f"port {p} ; pwm ; coast ; off ;" 293 | else: 294 | hexstr = ' '.join(f'{h:x}' for h in [0xc2, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 295 | self.write(f"port {p} ; write1 {hexstr}\r".encode()) 296 | self.write(f"{turnoff}\r".encode()) 297 | self.write(b"port 0 ; select ; port 1 ; select ; port 2 ; select ; port 3 ; select ; echo 0\r") 298 | 299 | def motorloop(self, q): 300 | """Event handling for non-blocking motor commands 301 | 302 | :param q: Queue of motor functions 303 | """ 304 | while self.running: 305 | func, data = q.get() 306 | if func is None: 307 | break 308 | else: 309 | func(*data) 310 | func = None # Necessary for 'del' to function correctly on motor object 311 | data = None 312 | q.task_done() 313 | 314 | def callbackloop(self, q): 315 | """Event handling for callbacks 316 | 317 | :param q: Queue of callback events 318 | """ 319 | while self.running: 320 | cb = q.get() 321 | # Test for empty tuple, which should only be passed when 322 | # we're shutting down 323 | if len(cb) == 0: 324 | continue 325 | if not cb[0]._alive: 326 | continue 327 | cb[0]()(cb[1]) 328 | q.task_done() 329 | 330 | def loop(self, cond, uselist, q, listevt): 331 | """Event handling for Build HAT 332 | 333 | :param cond: Condition used to block user's script till we're ready 334 | :param uselist: Whether we're using the HATs 'list' function or not 335 | :param q: Queue for callback events 336 | """ 337 | count = 0 338 | while self.running: 339 | line = self.read() 340 | if len(line) == 0: 341 | continue 342 | if line[0] == "P" and line[2] == ":": 343 | portid = int(line[1]) 344 | msg = line[2:] 345 | if cmp(msg, BuildHAT.CONNECTED): 346 | typeid = int(line[2 + len(BuildHAT.CONNECTED):], 16) 347 | self.connections[portid].update(typeid, True) 348 | if typeid == 64: 349 | self.write(f"port {portid} ; on\r".encode()) 350 | if uselist and listevt.is_set(): 351 | count += 1 352 | elif cmp(msg, BuildHAT.CONNECTEDPASSIVE): 353 | typeid = int(line[2 + len(BuildHAT.CONNECTEDPASSIVE):], 16) 354 | self.connections[portid].update(typeid, True) 355 | if uselist and listevt.is_set(): 356 | count += 1 357 | elif cmp(msg, BuildHAT.DISCONNECTED): 358 | self.connections[portid].update(-1, False) 359 | elif cmp(msg, BuildHAT.DEVTIMEOUT): 360 | self.connections[portid].update(-1, False) 361 | elif cmp(msg, BuildHAT.NOTCONNECTED): 362 | self.connections[portid].update(-1, False) 363 | if uselist and listevt.is_set(): 364 | count += 1 365 | elif cmp(msg, BuildHAT.RAMPDONE): 366 | ftr = self.rampftr[portid].pop() 367 | ftr.set_result(True) 368 | elif cmp(msg, BuildHAT.PULSEDONE): 369 | ftr = self.pulseftr[portid].pop() 370 | ftr.set_result(True) 371 | 372 | if uselist and count == 4: 373 | with cond: 374 | uselist = False 375 | cond.notify() 376 | 377 | if not uselist and cmp(line, BuildHAT.DONE): 378 | def runit(): 379 | with cond: 380 | cond.notify() 381 | t = Timer(8.0, runit) 382 | t.start() 383 | 384 | if line[0] == "P" and (line[2] == "C" or line[2] == "M"): 385 | portid = int(line[1]) 386 | data = line[5:].split(" ") 387 | newdata = [] 388 | for d in data: 389 | if "." in d: 390 | newdata.append(float(d)) 391 | else: 392 | if d != "": 393 | newdata.append(int(d)) 394 | # Check data was for our current mode 395 | if line[2] == "M" and self.connections[portid].simplemode != int(line[3]): 396 | continue 397 | elif line[2] == "C" and self.connections[portid].combimode != int(line[3]): 398 | continue 399 | callit = self.connections[portid].callit 400 | if callit is not None: 401 | q.put((callit, newdata)) 402 | self.connections[portid].data = newdata 403 | try: 404 | ftr = self.portftr[portid].pop() 405 | ftr.set_result(newdata) 406 | except IndexError: 407 | pass 408 | 409 | if len(line) >= 5 and line[1] == "." and line.endswith(" V"): 410 | vin = float(line.split(" ")[0]) 411 | ftr = self.vinftr.pop() 412 | ftr.set_result(vin) 413 | -------------------------------------------------------------------------------- /buildhat/wedo.py: -------------------------------------------------------------------------------- 1 | """WeDo sensor handling functionality""" 2 | 3 | from .devices import Device 4 | 5 | 6 | class TiltSensor(Device): 7 | """Tilt sensor 8 | 9 | :param port: Port of device 10 | :raises DeviceError: Occurs if there is no tilt sensor attached to port 11 | """ 12 | 13 | def __init__(self, port): 14 | """ 15 | Initialise tilt sensor 16 | 17 | :param port: Port of device 18 | """ 19 | super().__init__(port) 20 | self.mode(0) 21 | 22 | def get_tilt(self): 23 | """ 24 | Return the tilt from tilt sensor 25 | 26 | :return: Tilt from tilt sensor 27 | :rtype: tuple 28 | """ 29 | return tuple(self.get()) 30 | 31 | 32 | class MotionSensor(Device): 33 | """Motion sensor 34 | 35 | :param port: Port of device 36 | :raises DeviceError: Occurs if there is no motion sensor attached to port 37 | """ 38 | 39 | default_mode = 0 40 | 41 | def __init__(self, port): 42 | """ 43 | Initialise motion sensor 44 | 45 | :param port: Port of device 46 | """ 47 | super().__init__(port) 48 | self.mode(self.default_mode) 49 | 50 | def set_default_data_mode(self, mode): 51 | """ 52 | Set the mode most often queried from this device. 53 | 54 | This significantly improves performance when repeatedly accessing data 55 | 56 | :param mode: 0 for distance (default), 1 for movement count 57 | """ 58 | if mode == 1 or mode == 0: 59 | self.default_mode = mode 60 | self.mode(mode) 61 | 62 | def get_distance(self): 63 | """ 64 | Return the distance from motion sensor 65 | 66 | :return: Distance from motion sensor 67 | :rtype: int 68 | """ 69 | return self._get_data_from_mode(0) 70 | 71 | def get_movement_count(self): 72 | """ 73 | Return the movement counter 74 | 75 | This is the count of how many times the sensor has detected an object 76 | that moved within 4 blocks of the sensor since the sensor has been 77 | plugged in or the BuildHAT reset 78 | 79 | :return: Count of objects detected 80 | :rtype: int 81 | """ 82 | return self._get_data_from_mode(1) 83 | 84 | def _get_data_from_mode(self, mode): 85 | if self.default_mode == mode: 86 | return self.get()[0] 87 | else: 88 | self.mode(mode) 89 | retval = self.get()[0] 90 | self.mode(self.default_mode) 91 | return retval 92 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | # Heavily trimmed from the version used for Flipper 4 | 5 | PYTHON = python3 6 | BUILD = sphinx-build 7 | 8 | BUILDDIR = build 9 | GENRSTDIR = genrst 10 | 11 | # Force rebuilds from scratch for consistency 12 | FORCE = -E 13 | 14 | SFLAGS = -d $(BUILDDIR)/doctrees . 15 | 16 | 17 | .PHONY: clean html 18 | 19 | clean: 20 | rm -rf $(BUILDDIR)/* 21 | rm -f $(GENRSTDIR)/* 22 | 23 | html: 24 | $(BUILD) $(FORCE) -b html $(SFLAGS) $(BUILDDIR)/html 25 | 26 | latex: 27 | $(BUILD) $(FORCE) -b latex $(SFLAGS) $(BUILDDIR)/latex 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Build HAT Module Documentation 2 | ============================== 3 | 4 | This directory contains the documentation for the "buildhat" module 5 | that communicates with the Raspberry Pi Build HAT. The documentation 6 | is here as ReStructured Text source files, together with a Makefile 7 | for building it into HTML. Other builds can be added as required. 8 | 9 | Building the Documentation 10 | -------------------------- 11 | 12 | Getting the documentation converted from ReStructured Text to your 13 | preferred format requires the sphinx package, and optionally (for 14 | RTD-styling) sphinx\_rtd\_theme package. To install them: 15 | 16 | ``` 17 | pip install sphinx 18 | pip install sphinx_rtd_theme 19 | ``` 20 | 21 | If using asdf: 22 | 23 | ``` 24 | asdf reshim python 25 | ``` 26 | 27 | Then to build the documentation as HTML: 28 | 29 | ``` 30 | make html 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/buildhat/color.py: -------------------------------------------------------------------------------- 1 | """Example getting color values from color sensor""" 2 | 3 | from buildhat import ColorSensor 4 | 5 | color = ColorSensor('C') 6 | 7 | print("HSV", color.get_color_hsv()) 8 | print("RGBI", color.get_color_rgbi()) 9 | print("Ambient", color.get_ambient_light()) 10 | print("Reflected", color.get_reflected_light()) 11 | print("Color", color.get_color()) 12 | 13 | print("Waiting for color black") 14 | color.wait_until_color("black") 15 | print("Found color black") 16 | 17 | print("Waiting for color white") 18 | color.wait_until_color("white") 19 | print("Found color white") 20 | 21 | while True: 22 | c = color.wait_for_new_color() 23 | print("Found new color", c) 24 | -------------------------------------------------------------------------------- /docs/buildhat/colordistance.py: -------------------------------------------------------------------------------- 1 | """Example for color distance sensor""" 2 | 3 | from buildhat import ColorDistanceSensor 4 | 5 | color = ColorDistanceSensor('C') 6 | 7 | print("Distance", color.get_distance()) 8 | print("RGBI", color.get_color_rgb()) 9 | print("Ambient", color.get_ambient_light()) 10 | print("Reflected", color.get_reflected_light()) 11 | print("Color", color.get_color()) 12 | 13 | print("Waiting for color black") 14 | color.wait_until_color("black") 15 | print("Found color black") 16 | 17 | print("Waiting for color white") 18 | color.wait_until_color("white") 19 | print("Found color white") 20 | 21 | while True: 22 | c = color.wait_for_new_color() 23 | print("Found new color", c) 24 | -------------------------------------------------------------------------------- /docs/buildhat/colordistancesensor.rst: -------------------------------------------------------------------------------- 1 | ColorDistanceSensor 2 | =================== 3 | 4 | |location_link| 5 | 6 | .. |location_link| raw:: html 7 | 8 | LEGO Color and Distance Sensor 88007 9 | 10 | |location_link2| 11 | 12 | .. |location_link2| raw:: html 13 | 14 | BrickLink listing 15 | 16 | 17 | The LEGO® Color and Distance Sensor can sort between six different colors and objects within 5 to 10 cm range 18 | 19 | NOTE: Support for this device is experimental and not all features are available yet. 20 | 21 | .. autoclass:: buildhat.ColorDistanceSensor 22 | :members: 23 | :inherited-members: 24 | 25 | Example 26 | ------- 27 | 28 | .. literalinclude:: colordistance.py 29 | 30 | -------------------------------------------------------------------------------- /docs/buildhat/colorsensor.rst: -------------------------------------------------------------------------------- 1 | ColorSensor 2 | =========== 3 | 4 | .. image:: images/45605_prod_TECHNIC_Color_Sensor_01_200.png 5 | :width: 200 6 | :alt: The LEGO Color Sensor 7 | 8 | |location_link| 9 | 10 | .. |location_link| raw:: html 11 | 12 | LEGO Color Sensor 45605 13 | 14 | |location_link2| 15 | 16 | .. |location_link2| raw:: html 17 | 18 | BrickLink item 19 | 20 | 21 | The LEGO® Education SPIKE™ Color Sensor can sort between eight different colors and can measure reflected and ambient or natural light. 22 | 23 | .. autoclass:: buildhat.ColorSensor 24 | :members: 25 | :inherited-members: 26 | 27 | Example 28 | ------- 29 | 30 | .. literalinclude:: color.py 31 | 32 | -------------------------------------------------------------------------------- /docs/buildhat/distance.py: -------------------------------------------------------------------------------- 1 | """Example for distance sensor""" 2 | 3 | from signal import pause 4 | 5 | from buildhat import DistanceSensor, Motor 6 | 7 | motor = Motor('A') 8 | dist = DistanceSensor('D', threshold_distance=100) 9 | 10 | print("Wait for in range") 11 | dist.wait_for_in_range(50) 12 | motor.run_for_rotations(1) 13 | 14 | print("Wait for out of range") 15 | dist.wait_for_out_of_range(100) 16 | motor.run_for_rotations(2) 17 | 18 | 19 | def handle_in(distance): 20 | """Within range 21 | 22 | :param distance: Distance 23 | """ 24 | print("in range", distance) 25 | 26 | 27 | def handle_out(distance): 28 | """Out of range 29 | 30 | :param distance: Distance 31 | """ 32 | print("out of range", distance) 33 | 34 | 35 | dist.when_in_range = handle_in 36 | dist.when_out_of_range = handle_out 37 | pause() 38 | -------------------------------------------------------------------------------- /docs/buildhat/distancesensor.rst: -------------------------------------------------------------------------------- 1 | DistanceSensor 2 | ============== 3 | 4 | .. image:: images/45604_prod_TECHNIC_Distance_Sensor_01_200.png 5 | :width: 200 6 | :alt: The LEGO Distance Sensor 7 | 8 | |location_link| 9 | 10 | .. |location_link| raw:: html 11 | 12 | LEGO Distance Sensor 45604 13 | 14 | |location_link2| 15 | 16 | .. |location_link2| raw:: html 17 | 18 | BrickLink item 19 | 20 | The LEGO® Education SPIKE™ Distance Sensor behaves like a conventional ultrasonic range finder but also has four LEDs that can be used to create the "eyes" of a robot. Each LED can be controlled individually. 21 | 22 | .. autoclass:: buildhat.DistanceSensor 23 | :members: 24 | :inherited-members: 25 | 26 | Example 27 | ------- 28 | 29 | .. literalinclude:: distance.py 30 | -------------------------------------------------------------------------------- /docs/buildhat/force.py: -------------------------------------------------------------------------------- 1 | """Example using force sensor""" 2 | 3 | from signal import pause 4 | 5 | from buildhat import ForceSensor, Motor 6 | 7 | motor = Motor('A') 8 | button = ForceSensor('D', threshold_force=1) 9 | 10 | print("Waiting for button to be pressed fully and released") 11 | 12 | button.wait_until_pressed(100) 13 | button.wait_until_released(0) 14 | 15 | motor.run_for_rotations(1) 16 | 17 | print("Wait for button to be pressed") 18 | 19 | button.wait_until_pressed() 20 | motor.run_for_rotations(2) 21 | 22 | 23 | def handle_pressed(force): 24 | """Force sensor pressed 25 | 26 | :param force: Force value 27 | """ 28 | print("pressed", force) 29 | 30 | 31 | def handle_released(force): 32 | """Force sensor released 33 | 34 | :param force: Force value 35 | """ 36 | print("released", force) 37 | 38 | 39 | button.when_pressed = handle_pressed 40 | button.when_released = handle_released 41 | pause() 42 | -------------------------------------------------------------------------------- /docs/buildhat/forcesensor.rst: -------------------------------------------------------------------------------- 1 | ForceSensor 2 | =========== 3 | 4 | .. image:: images/45606_prod_TECHNIC_Force_Sensor_01_200.png 5 | :width: 200 6 | :alt: The LEGO Force Sensor 7 | 8 | |location_link| 9 | 10 | .. |location_link| raw:: html 11 | 12 | LEGO® Force Sensor Set 45606e 13 | 14 | |location_link2| 15 | 16 | .. |location_link2| raw:: html 17 | 18 | BrickLink item 19 | 20 | 21 | The LEGO® Education SPIKE™ Prime Force Sensor (also known as the LEGO® Technic Force Sensor) can measure pressure of up to 10 newtons (N), but it can also be used as a touch sensor or a simple button. 22 | 23 | 24 | .. autoclass:: buildhat.ForceSensor 25 | :members: 26 | :inherited-members: 27 | 28 | Example 29 | ------- 30 | 31 | .. literalinclude:: force.py 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/buildhat/hat.py: -------------------------------------------------------------------------------- 1 | """Example to print devices attached to hat""" 2 | 3 | from buildhat import Hat 4 | 5 | hat = Hat() 6 | print(hat.get()) 7 | -------------------------------------------------------------------------------- /docs/buildhat/hat.rst: -------------------------------------------------------------------------------- 1 | Hat 2 | === 3 | 4 | Gets list of devices connected to the hat 5 | 6 | .. autoclass:: buildhat.Hat 7 | :members: 8 | :inherited-members: 9 | 10 | Example 11 | ------- 12 | 13 | .. literalinclude:: hat.py 14 | -------------------------------------------------------------------------------- /docs/buildhat/images/45602_prod_TECHNIC_Large_Angular_Motor_01_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/45602_prod_TECHNIC_Large_Angular_Motor_01_200.png -------------------------------------------------------------------------------- /docs/buildhat/images/45603_prod_TECHNIC_Medium_Angular_Motor_01_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/45603_prod_TECHNIC_Medium_Angular_Motor_01_200.png -------------------------------------------------------------------------------- /docs/buildhat/images/45604_prod_TECHNIC_Distance_Sensor_01_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/45604_prod_TECHNIC_Distance_Sensor_01_200.png -------------------------------------------------------------------------------- /docs/buildhat/images/45605_prod_TECHNIC_Color_Sensor_01_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/45605_prod_TECHNIC_Color_Sensor_01_200.png -------------------------------------------------------------------------------- /docs/buildhat/images/45606_prod_TECHNIC_Force_Sensor_01_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/45606_prod_TECHNIC_Force_Sensor_01_200.png -------------------------------------------------------------------------------- /docs/buildhat/images/45607_Prod_01_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/45607_Prod_01_200.png -------------------------------------------------------------------------------- /docs/buildhat/images/45608_Prod_01_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/45608_Prod_01_200.png -------------------------------------------------------------------------------- /docs/buildhat/images/88008_01_200.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/88008_01_200.jpeg -------------------------------------------------------------------------------- /docs/buildhat/images/88013_motor_l_02_200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/88013_motor_l_02_200.jpg -------------------------------------------------------------------------------- /docs/buildhat/images/88014_motor_xl_03_200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/88014_motor_xl_03_200.jpg -------------------------------------------------------------------------------- /docs/buildhat/images/BuildHAT_closeup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/BuildHAT_closeup.jpg -------------------------------------------------------------------------------- /docs/buildhat/images/lights.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/lights.jpg -------------------------------------------------------------------------------- /docs/buildhat/images/lpf2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/lpf2.jpg -------------------------------------------------------------------------------- /docs/buildhat/images/train_motor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/buildhat/images/train_motor.jpg -------------------------------------------------------------------------------- /docs/buildhat/index.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Build HAT 3 | ================ 4 | 5 | .. _buildhat_lib: 6 | 7 | Library 8 | ======= 9 | 10 | The Build HAT library has been created to support the `Raspberry Pi Build HAT`_, 11 | an add-on board for the Raspberry Pi computer, which allows control of up to four LEGO® TECHNIC™ motors and sensors included in the SPIKE™ Portfolio. 12 | 13 | .. _Raspberry Pi Build HAT: http://raspberrypi.com/products/build-hat 14 | 15 | .. image:: images/BuildHAT_closeup.jpg 16 | :width: 300 17 | :alt: The Raspberry Pi Build HAT 18 | 19 | 20 | Other LEGO® devices may be supported if they use the PoweredUp connector: 21 | 22 | .. image:: images/lpf2.jpg 23 | :width: 100 24 | :alt: The LEGO PoweredUp connector 25 | 26 | In order to drive motors, your Raspberry Pi and Build HAT will need an external 7.5V 27 | power supply. For best results, use the `official Raspberry Pi Build HAT power supply`_. 28 | 29 | .. _official Raspberry Pi Build HAT power supply: http://raspberrypi.com/products/build-hat-power-supply 30 | 31 | .. warning:: 32 | 33 | The API for the Build HAT is undergoing active development and is subject 34 | to change. 35 | 36 | .. toctree:: 37 | :maxdepth: 2 38 | 39 | colorsensor.rst 40 | colordistancesensor.rst 41 | distancesensor.rst 42 | forcesensor.rst 43 | light.rst 44 | matrix.rst 45 | motionsensor.rst 46 | motor.rst 47 | motorpair.rst 48 | passivemotor.rst 49 | tiltsensor.rst 50 | hat.rst 51 | -------------------------------------------------------------------------------- /docs/buildhat/light.py: -------------------------------------------------------------------------------- 1 | """Example turning on/off LED lights""" 2 | 3 | from time import sleep 4 | 5 | from buildhat import Light 6 | 7 | light = Light('A') 8 | 9 | light.on() 10 | sleep(1) 11 | light.off() 12 | -------------------------------------------------------------------------------- /docs/buildhat/light.rst: -------------------------------------------------------------------------------- 1 | Light 2 | =========== 3 | 4 | .. image:: images/lights.jpg 5 | :width: 200 6 | :alt: The LEGO Light 7 | 8 | |location_link| 9 | 10 | .. |location_link| raw:: html 11 | 12 | LEGO® Light 88005 13 | 14 | |location_link2| 15 | 16 | .. |location_link2| raw:: html 17 | 18 | BrickLink item 19 | 20 | 21 | The LEGO® 88005 Powered Up Light features 2 LED lights, connecting wire and connection point for LEGO® Powered Up components. 22 | 23 | 24 | .. autoclass:: buildhat.Light 25 | :members: 26 | :inherited-members: 27 | 28 | Example 29 | ------- 30 | 31 | .. literalinclude:: light.py 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/buildhat/matrix.py: -------------------------------------------------------------------------------- 1 | """Example driving LED matrix""" 2 | 3 | import random 4 | import time 5 | 6 | from buildhat import Matrix 7 | 8 | matrix = Matrix('C') 9 | 10 | matrix.clear(("red", 10)) 11 | time.sleep(1) 12 | 13 | matrix.clear() 14 | time.sleep(1) 15 | 16 | matrix.set_pixel((0, 0), ("blue", 10)) 17 | matrix.set_pixel((2, 2), ("red", 10)) 18 | time.sleep(1) 19 | 20 | while True: 21 | out = [[(int(random.uniform(0, 9)), 10) for x in range(3)] for y in range(3)] 22 | matrix.set_pixels(out) 23 | time.sleep(0.1) 24 | -------------------------------------------------------------------------------- /docs/buildhat/matrix.rst: -------------------------------------------------------------------------------- 1 | Matrix 2 | ====== 3 | 4 | .. image:: images/45608_Prod_01_200.png 5 | :width: 200 6 | :alt: The LEGO 3x3 LED matrix 7 | 8 | The LEGO® SPIKE™ 3x3 LED matrix has individual elements that can be set individually or as a whole. 9 | 10 | Colours may be passed as string or integer parameters. 11 | 12 | .. list-table:: LED Matrix colours 13 | :widths: 10 20 14 | :header-rows: 1 15 | 16 | * - Number 17 | - Name 18 | * - 0 19 | - 20 | * - 1 21 | - pink 22 | * - 2 23 | - lilac 24 | * - 3 25 | - blue 26 | * - 4 27 | - cyan 28 | * - 5 29 | - turquoise 30 | * - 6 31 | - green 32 | * - 7 33 | - yellow 34 | * - 8 35 | - orange 36 | * - 9 37 | - red 38 | * - 10 39 | - white 40 | 41 | .. autoclass:: buildhat.Matrix 42 | :members: 43 | :inherited-members: 44 | 45 | Example 46 | ------- 47 | 48 | .. literalinclude:: matrix.py 49 | -------------------------------------------------------------------------------- /docs/buildhat/motion.py: -------------------------------------------------------------------------------- 1 | """Example using motion sensor""" 2 | 3 | from time import sleep 4 | 5 | from buildhat import MotionSensor 6 | 7 | motion = MotionSensor('A') 8 | 9 | for _ in range(50): 10 | print(motion.get_distance()) 11 | sleep(0.1) 12 | -------------------------------------------------------------------------------- /docs/buildhat/motionsensor.rst: -------------------------------------------------------------------------------- 1 | Motion Sensor 2 | ============= 3 | 4 | |location_link| 5 | 6 | .. |location_link| raw:: html 7 | 8 | BrickLink item 9 | 10 | .. autoclass:: buildhat.MotionSensor 11 | :members: 12 | :inherited-members: 13 | 14 | Example 15 | ------- 16 | 17 | .. literalinclude:: motion.py 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/buildhat/motor.py: -------------------------------------------------------------------------------- 1 | """Example driving motors""" 2 | 3 | import time 4 | 5 | from buildhat import Motor 6 | 7 | motor = Motor('A') 8 | motorb = Motor('B') 9 | 10 | 11 | def handle_motor(speed, pos, apos): 12 | """Motor data 13 | 14 | :param speed: Speed of motor 15 | :param pos: Position of motor 16 | :param apos: Absolute position of motor 17 | """ 18 | print("Motor", speed, pos, apos) 19 | 20 | 21 | motor.when_rotated = handle_motor 22 | motor.set_default_speed(50) 23 | 24 | print("Run for degrees 360") 25 | motor.run_for_degrees(360) 26 | time.sleep(3) 27 | 28 | print("Run for degrees -360") 29 | motor.run_for_degrees(-360) 30 | time.sleep(3) 31 | 32 | print("Start motor") 33 | motor.start() 34 | time.sleep(3) 35 | print("Stop motor") 36 | motor.stop() 37 | time.sleep(1) 38 | 39 | print("Run for degrees - 180") 40 | motor.run_for_degrees(180) 41 | time.sleep(3) 42 | 43 | print("Run for degrees - 90") 44 | motor.run_for_degrees(90) 45 | time.sleep(3) 46 | 47 | print("Run for rotations - 2") 48 | motor.run_for_rotations(2) 49 | time.sleep(3) 50 | 51 | print("Run for seconds - 5") 52 | motor.run_for_seconds(5) 53 | time.sleep(3) 54 | 55 | print("Run both") 56 | motor.run_for_seconds(5, blocking=False) 57 | motorb.run_for_seconds(5, blocking=False) 58 | time.sleep(10) 59 | 60 | print("Run to position -90") 61 | motor.run_to_position(-90) 62 | time.sleep(3) 63 | 64 | print("Run to position 90") 65 | motor.run_to_position(90) 66 | time.sleep(3) 67 | 68 | print("Run to position 180") 69 | motor.run_to_position(180) 70 | time.sleep(3) 71 | -------------------------------------------------------------------------------- /docs/buildhat/motor.rst: -------------------------------------------------------------------------------- 1 | Motor 2 | ===== 3 | 4 | LEGO® TECHNIC™ motors from the LEGO® Education SPIKE™ portfolio have an integrated rotation sensor (encoder) and can be positioned with 1-degree accuracy. The encoders can be queried to find the current position of the motor with respect to a 'zero' mark shown on the motor itself. 5 | 6 | 7 | .. |location_link1| raw:: html 8 | 9 | LEGO® Large angular motor 45602 10 | 11 | .. |location_link1b| raw:: html 12 | 13 | BrickLink item 45602 14 | 15 | .. |location_link2| raw:: html 16 | 17 | LEGO® Medium angular motor 45603 18 | 19 | .. |location_link2b| raw:: html 20 | 21 | BrickLink item 45603 22 | 23 | .. |LAM| image:: images/45602_prod_TECHNIC_Large_Angular_Motor_01_200.png 24 | :width: 200 25 | :alt: The LEGO Large Angular Motor 26 | 27 | .. |MAM| image:: images/45603_prod_TECHNIC_Medium_Angular_Motor_01_200.png 28 | :width: 200 29 | :alt: The LEGO Medium Angular Motor 30 | 31 | .. |SAM| image:: images/45607_Prod_01_200.png 32 | :width: 200 33 | :alt: The LEGO Small Angular Motor 34 | 35 | .. list-table:: Motors with encoders 36 | :widths: 50 50 200 37 | :header-rows: 0 38 | 39 | 40 | * - |location_link1| 41 | - |location_link1b| 42 | - |LAM| 43 | * - |location_link2| 44 | - |location_link2b| 45 | - |MAM| 46 | * - LEGO® Small angular motor 45607 47 | - 48 | - |SAM| 49 | 50 | Other motors without encoders will report a 0 value if queried. 51 | 52 | 53 | .. |location_link3| raw:: html 54 | 55 | LEGO® Medium Linear motor 88008 56 | 57 | .. |location_link3b| raw:: html 58 | 59 | BrickLink item 88008 60 | 61 | .. |location_link4| raw:: html 62 | 63 | Technic™ Large Motor 88013 64 | 65 | .. |location_link4b| raw:: html 66 | 67 | BrickLink item 88013 68 | 69 | .. |location_link5| raw:: html 70 | 71 | Technic™ XL Motor 88014 72 | 73 | .. |location_link5b| raw:: html 74 | 75 | BrickLink item 88014 76 | 77 | .. |M88008| image:: images/88008_01_200.jpeg 78 | :width: 200 79 | :alt: The LEGO medium linear 88008 Motor 80 | 81 | .. |M88013| image:: images/88013_motor_l_02_200.jpg 82 | :width: 200 83 | :alt: The LEGO large linear 88013 Motor 84 | 85 | .. |M88014| image:: images/88014_motor_xl_03_200.jpg 86 | :width: 200 87 | :alt: The LEGO XL linear 88014 Motor 88 | 89 | .. list-table:: Motors without encoders 90 | :widths: 50 50 200 91 | :header-rows: 0 92 | 93 | 94 | * - |location_link3| 95 | - |location_link3b| 96 | - |M88008| 97 | * - |location_link4| 98 | - |location_link4b| 99 | - |M88013| 100 | * - |location_link5| 101 | - |location_link5b| 102 | - |M88014| 103 | 104 | 105 | 106 | 107 | .. autoclass:: buildhat.Motor 108 | :members: 109 | :inherited-members: 110 | 111 | Example 112 | ------- 113 | 114 | .. literalinclude:: motor.py 115 | 116 | -------------------------------------------------------------------------------- /docs/buildhat/motorpair.py: -------------------------------------------------------------------------------- 1 | """Example for pair of motors""" 2 | 3 | from buildhat import MotorPair 4 | 5 | pair = MotorPair('C', 'D') 6 | pair.set_default_speed(20) 7 | pair.run_for_rotations(2) 8 | 9 | pair.run_for_rotations(1, speedl=100, speedr=20) 10 | 11 | pair.run_to_position(20, 100, speed=20) 12 | -------------------------------------------------------------------------------- /docs/buildhat/motorpair.rst: -------------------------------------------------------------------------------- 1 | MotorPair 2 | ========= 3 | 4 | .. autoclass:: buildhat.MotorPair 5 | :members: 6 | :inherited-members: 7 | 8 | Example 9 | ------- 10 | 11 | .. literalinclude:: motorpair.py 12 | -------------------------------------------------------------------------------- /docs/buildhat/passivemotor.py: -------------------------------------------------------------------------------- 1 | """Passive motor example""" 2 | 3 | import time 4 | 5 | from buildhat import PassiveMotor 6 | 7 | motor = PassiveMotor('A') 8 | 9 | print("Start motor") 10 | motor.start() 11 | time.sleep(3) 12 | print("Stop motor") 13 | motor.stop() 14 | -------------------------------------------------------------------------------- /docs/buildhat/passivemotor.rst: -------------------------------------------------------------------------------- 1 | PassiveMotor 2 | ============ 3 | 4 | LEGO® TECHNIC™ motors which do not have an integrated rotation sensor (encoder). 5 | 6 | 7 | .. |location_link1| raw:: html 8 | 9 | LEGO® Train motor 88011 10 | 11 | .. |location_link1b| raw:: html 12 | 13 | BrickLink item 88011 14 | 15 | 16 | .. |LTM| image:: images/train_motor.jpg 17 | :width: 200 18 | :alt: The LEGO Train Motor 19 | 20 | 21 | .. list-table:: Passive Motors 22 | :widths: 50 50 200 23 | :header-rows: 0 24 | 25 | 26 | * - |location_link1| 27 | - |location_link1b| 28 | - |LTM| 29 | 30 | 31 | 32 | .. autoclass:: buildhat.PassiveMotor 33 | :members: 34 | :inherited-members: 35 | 36 | Example 37 | ------- 38 | 39 | .. literalinclude:: passivemotor.py 40 | 41 | -------------------------------------------------------------------------------- /docs/buildhat/tilt.py: -------------------------------------------------------------------------------- 1 | """Example using tilt sensor""" 2 | 3 | from time import sleep 4 | 5 | from buildhat import TiltSensor 6 | 7 | tilt = TiltSensor('A') 8 | 9 | for _ in range(50): 10 | print(tilt.get_tilt()) 11 | sleep(0.1) 12 | -------------------------------------------------------------------------------- /docs/buildhat/tiltsensor.rst: -------------------------------------------------------------------------------- 1 | Tilt Sensor 2 | =========== 3 | 4 | |location_link| 5 | 6 | .. |location_link| raw:: html 7 | 8 | BrickLink item 9 | 10 | .. autoclass:: buildhat.TiltSensor 11 | :members: 12 | :inherited-members: 13 | 14 | Example 15 | ------- 16 | 17 | .. literalinclude:: tilt.py 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import os 6 | from collections import OrderedDict 7 | 8 | sys.path.insert(0, os.path.abspath('.')) 9 | 10 | # Need this to correctly find the docstrings for python modules 11 | sys.path.insert(0, os.path.abspath('..')) 12 | 13 | # Common strings 14 | PORT = 'hub' 15 | VERSION = 'latest' 16 | 17 | tags.add('port_' + PORT) 18 | ports = OrderedDict(( 19 | ('hub', 'the Build HAT'), 20 | )) 21 | 22 | # The members of the html_context dict are available inside 23 | # topindex.html 24 | 25 | URL_PATTERN = '//en/%s/%s' 26 | HTML_CONTEXT = { 27 | 'port': PORT, 28 | 'port_name': ports[PORT], 29 | 'port_version': VERSION, 30 | 'all_ports': [ (port_id, URL_PATTERN % (VERSION, port_id)) 31 | for port_id, port_name in ports.items() ], 32 | 'all_versions': [ VERSION, URL_PATTERN % (VERSION, PORT) ], 33 | 'downloads': [ ('PDF', URL_PATTERN % (VERSION, 34 | 'python-%s.pdf' % PORT)) ] 35 | } 36 | 37 | 38 | # Specify a custom master document based on the port name 39 | master_doc = PORT + "_index" 40 | 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.intersphinx', 45 | 'sphinx.ext.todo', 46 | 'sphinx.ext.coverage', 47 | 'sphinx_selective_exclude.modindex_exclude', 48 | 'sphinx_selective_exclude.eager_only', 49 | 'sphinx_selective_exclude.search_auto_exclude', 50 | 'sphinxcontrib.cmtinc-buildhat', 51 | ] 52 | 53 | 54 | # Add any paths that contain templates here, relative to this directory. 55 | templates_path = ['templates'] 56 | 57 | # The suffix of source filenames. 58 | source_suffix = '.rst' 59 | 60 | # General information about the project. 61 | project = 'Raspberry Pi Build HAT' 62 | copyright = '''2020-2021 - Raspberry Pi Foundation; 63 | 2017-2020 - LEGO System A/S - Aastvej 1, 7190 Billund, DK.''' 64 | 65 | # The version info for the project you're documenting, acts as replacement for 66 | # |version| and |release|, also used in various other places throughout the 67 | # built documents. 68 | # 69 | # We don't follow "The short X.Y version" vs "The full version, including alpha/beta/rc tags" 70 | # breakdown, so use the same version identifier for both to avoid confusion. 71 | with open(os.path.join(os.path.dirname(__file__),'../VERSION')) as versionf: 72 | version = versionf.read().strip() 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ['build', '.venv'] 77 | exclude_patterns += ['pending'] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all 80 | # documents. 81 | default_role = 'any' 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # Global include files. Sphinx docs suggest using rst_epilog in preference 87 | # of rst_prolog, so we follow. Absolute paths below mean "from the base 88 | # of the doctree". 89 | rst_epilog = """ 90 | .. include:: /templates/replace.inc 91 | """ 92 | 93 | # -- Options for HTML output ---------------------------------------------- 94 | 95 | # on_rtd is whether we are on readthedocs.org 96 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 97 | 98 | if not on_rtd: # only import and set the theme if we're building docs locally 99 | try: 100 | import sphinx_rtd_theme 101 | extensions.append("sphinx_rtd_theme") 102 | html_theme = 'sphinx_rtd_theme' 103 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), '.'] 104 | except: 105 | html_theme = 'default' 106 | html_theme_path = ['.'] 107 | else: 108 | html_theme_path = ['.'] 109 | 110 | # The name of an image file (within the static path) to use as favicon of the 111 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 112 | # pixels large. 113 | html_favicon = 'build/html/_static/favicon.ico' 114 | 115 | # Add any paths that contain custom static files (such as style sheets) here, 116 | # relative to this directory. They are copied after the builtin static files, 117 | # so a file named "default.css" will overwrite the builtin "default.css". 118 | html_static_path = ['static'] 119 | 120 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 121 | # using the given strftime format. 122 | html_last_updated_fmt = '%d %b %Y' 123 | 124 | # Additional templates that should be rendered to pages, maps page names to 125 | # template names. 126 | html_additional_pages = {"index": "topindex.html"} 127 | 128 | # Output file base name for HTML help builder. 129 | htmlhelp_basename = 'BuildHATdoc' 130 | -------------------------------------------------------------------------------- /docs/hub_index.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Build HAT 3 | ================ 4 | 5 | Build HAT documentation and references 6 | ====================================== 7 | 8 | .. toctree:: 9 | 10 | buildhat/index.rst 11 | license.rst 12 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License information 2 | =================== 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2020-2021 - Raspberry Pi Foundation 7 | 8 | Copyright (c) 2017-2021 - LEGO System A/S - Aastvej 1, 7190 Billund, DK 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /docs/sphinx_selective_exclude/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 by the sphinx_selective_exclude authors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /docs/sphinx_selective_exclude/README.md: -------------------------------------------------------------------------------- 1 | Sphinx eager ".. only::" directive and other selective rendition extensions 2 | =========================================================================== 3 | 4 | Project home page: https://github.com/pfalcon/sphinx_selective_exclude 5 | 6 | The implementation of ".. only::" directive in Sphinx documentation 7 | generation tool is known to violate principles of least user surprise 8 | and user expectations in general. Instead of excluding content early 9 | in the pipeline (pre-processor style), Sphinx defers exclusion until 10 | output phase, and what's the worst, various stages processing ignore 11 | "only" blocks and their exclusion status, so they may leak unexpected 12 | information into ToC, indexes, etc. 13 | 14 | There's multiple issues submitted upstream on this matter: 15 | 16 | * https://github.com/sphinx-doc/sphinx/issues/2150 17 | * https://github.com/sphinx-doc/sphinx/issues/1717 18 | * https://github.com/sphinx-doc/sphinx/issues/1488 19 | * etc. 20 | 21 | They are largely ignored by Sphinx maintainers. 22 | 23 | This projects tries to rectify situation on users' side. It actually 24 | changes the way Sphinx processes "only" directive, but does this 25 | without forking the project, and instead is made as a standard 26 | Sphinx extension, which a user may add to their documentation config. 27 | Unlike normal extensions, extensions provided in this package 28 | monkey-patch Sphinx core to work in a way expected by users. 29 | 30 | eager_only 31 | ---------- 32 | 33 | The core extension provided by the package is called `eager_only` and 34 | is based on the idea by Andrea Cassioli (see bugreports above) to 35 | process "only" directive as soon as possible during parsing phase. 36 | This approach has some drawbacks, like producing warnings like 37 | "WARNING: document isn't included in any toctree" if "only" is used 38 | to shape up a toctree, or the fact that changing a documentation 39 | builder (html/latex/etc.) will almost certainly require complete 40 | rebuild of documentation. But these are relatively minor issues 41 | comparing to completely broken way "only" works in upstream Sphinx. 42 | 43 | modindex_exclude 44 | ---------------- 45 | 46 | "only" directive allows for fine-grained conditional exclusion, but 47 | sometimes you may want to exclude entire module(s) at once. Even if 48 | you wrap an entire module description in "only" directive, like: 49 | 50 | .. only: option1 51 | .. module:: my_module 52 | 53 | ... 54 | 55 | You will still have an HTML page generated, albeit empty. It may also 56 | go into indexes, so will be discoverable by users, leading to less 57 | than ideal experience. `modindex_exclude` extension is design to 58 | resolve this issue, by making sure that any reference of a module 59 | is excluded from Python module index ("modindex"), as well as 60 | general cross-reference index ("genindex"). In the latter case, 61 | any symbol belong to a module will be excluded. Unlike `eager_only` 62 | extension which appear to have issued with "latexpdf" builder, 63 | `modindex_exclude` is useful for PDF, and allows to get cleaner 64 | index for PDF, just the same as for HTML. 65 | 66 | search_auto_exclude 67 | ------------------- 68 | 69 | Even if you exclude some documents from toctree:: using only:: 70 | directive, they will be indexed for full-text search, so user may 71 | find them and get confused. This plugin follows very simple idea 72 | that if you didn't include some documents in the toctree, then 73 | you didn't want them to be accessible (e.g. for a particular 74 | configuration), and so will make sure they aren't indexed either. 75 | 76 | This extension depends on `eager_only` and won't work without it. 77 | Note that Sphinx will issue warnings, as usual, for any documents 78 | not included in a toctree. This is considered a feature, and gives 79 | you a chance to check that document exclusions are indeed right 80 | for a particular configuration you build (and not that you forgot 81 | to add something to a toctree). 82 | 83 | Summary 84 | ------- 85 | 86 | Based on the above, sphinx_selective_exclude offers extension to let 87 | you: 88 | 89 | * Make "only::" directive work in an expected, intuitive manner, using 90 | `eager_only` extension. 91 | * However, if you apply only:: to toctree::, excluded documents will 92 | still be available via full-text search, so you need to use 93 | `search_auto_exclude` for that to work as expected. 94 | * Similar to search, indexes may also require special treatment, hence 95 | there's the `modindex_exclude` extension. 96 | 97 | Most likely, you will want to use all 3 extensions together - if you 98 | really want build subsets of docimentation covering sufficiently different 99 | configurations from a single doctree. However, if one of them is enough 100 | to cover your usecase, that's OK to (and why they were separated into 101 | 3 extensions, to follow KISS and "least surprise" principles and to 102 | not make people deal with things they aren't interested in). In this case, 103 | however remember there're other extensions, if you later hit a usecase 104 | when they're needed. 105 | 106 | Usage 107 | ----- 108 | 109 | To use these extensions, add https://github.com/pfalcon/sphinx_selective_exclude 110 | as a git submodule to your project, in documentation folder (where 111 | Sphinx conf.py is located). Alternatively, commit sphinx_selective_exclude 112 | directory instead of making it a submodule (you will need to pick up 113 | any project updates manually then). 114 | 115 | Add following lines to "extensions" settings in your conf.py (you 116 | likely already have some standard Sphinx extensions enabled): 117 | 118 | extensions = [ 119 | ... 120 | 'sphinx_selective_exclude.eager_only', 121 | 'sphinx_selective_exclude.search_auto_exclude', 122 | 'sphinx_selective_exclude.modindex_exclude', 123 | ] 124 | 125 | As discussed above, you may enable all extensions, or one by one. 126 | 127 | Please note that to make sure these extensions work well and avoid producing 128 | output docs with artifacts, it is IMPERATIVE to remove cached doctree if 129 | you rebuild documentation with another builder (i.e. with different output 130 | format). Also, to stay on safe side, it's recommended to remove old doctree 131 | anyway before generating production-ready documentation for publishing. To 132 | do that, run something like: 133 | 134 | rm -rf _build/doctrees/ 135 | 136 | A typical artificat when not following these simple rules is that content 137 | of some sections may be missing. If you face anything like that, just 138 | remember what's written above and remove cached doctrees. 139 | -------------------------------------------------------------------------------- /docs/sphinx_selective_exclude/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/sphinx_selective_exclude/__init__.py -------------------------------------------------------------------------------- /docs/sphinx_selective_exclude/eager_only.py: -------------------------------------------------------------------------------- 1 | # 2 | # This is a Sphinx documentation tool extension which makes .only:: 3 | # directives be eagerly processed early in the parsing stage. This 4 | # makes sure that content in .only:: blocks gets actually excluded 5 | # as a typical user expects, instead of bits of information in 6 | # these blocks leaking to documentation in various ways (e.g., 7 | # indexes containing entries for functions which are actually in 8 | # .only:: blocks and thus excluded from documentation, etc.) 9 | # Note that with this extension, you may need to completely 10 | # rebuild a doctree when switching builders (i.e. completely 11 | # remove _build/doctree dir between generation of HTML vs PDF 12 | # documentation). 13 | # 14 | # This extension works by monkey-patching Sphinx core, so potentially 15 | # may not work with untested Sphinx versions. It tested to work with 16 | # 1.2.2 and 1.4.2 17 | # 18 | # Copyright (c) 2016 Paul Sokolovsky 19 | # Based on idea by Andrea Cassioli: 20 | # https://github.com/sphinx-doc/sphinx/issues/2150#issuecomment-171912290 21 | # Licensed under the terms of BSD license, see LICENSE file. 22 | # 23 | import sphinx 24 | from docutils.parsers.rst import directives 25 | 26 | 27 | class EagerOnly(sphinx.directives.other.Only): 28 | 29 | def run(self, *args): 30 | # Evaluate the condition eagerly, and if false return no nodes right away 31 | env = self.state.document.settings.env 32 | env.app.builder.tags.add('TRUE') 33 | #print(repr(self.arguments[0])) 34 | if not env.app.builder.tags.eval_condition(self.arguments[0]): 35 | return [] 36 | 37 | # Otherwise, do the usual processing 38 | nodes = super(EagerOnly, self).run() 39 | if len(nodes) == 1: 40 | nodes[0]['expr'] = 'TRUE' 41 | return nodes 42 | 43 | 44 | def setup(app): 45 | directives.register_directive('only', EagerOnly) 46 | -------------------------------------------------------------------------------- /docs/sphinx_selective_exclude/modindex_exclude.py: -------------------------------------------------------------------------------- 1 | # 2 | # This is a Sphinx documentation tool extension which allows to 3 | # exclude some Python modules from the generated indexes. Modules 4 | # are excluded both from "modindex" and "genindex" index tables 5 | # (in the latter case, all members of a module are excluded). 6 | # To control exclusion, set "modindex_exclude" variable in Sphinx 7 | # conf.py to the list of modules to exclude. Note: these should be 8 | # modules (as defined by py:module directive, not just raw filenames). 9 | # This extension works by monkey-patching Sphinx core, so potentially 10 | # may not work with untested Sphinx versions. It tested to work with 11 | # 1.2.2 and 1.4.2 12 | # 13 | # Copyright (c) 2016 Paul Sokolovsky 14 | # Licensed under the terms of BSD license, see LICENSE file. 15 | # 16 | import sphinx 17 | 18 | 19 | #org_PythonModuleIndex_generate = None 20 | org_PyObject_add_target_and_index = None 21 | org_PyModule_run = None 22 | 23 | EXCLUDES = {} 24 | 25 | # No longer used, PyModule_run() monkey-patch does all the job 26 | def PythonModuleIndex_generate(self, docnames=None): 27 | docnames = [] 28 | excludes = self.domain.env.config['modindex_exclude'] 29 | for modname, (docname, synopsis, platforms, deprecated) in self.domain.data['modules'].items(): 30 | #print(docname) 31 | if modname not in excludes: 32 | docnames.append(docname) 33 | 34 | return org_PythonModuleIndex_generate(self, docnames) 35 | 36 | 37 | def PyObject_add_target_and_index(self, name_cls, sig, signode): 38 | if hasattr(self.env, "ref_context"): 39 | # Sphinx 1.4 40 | ref_context = self.env.ref_context 41 | else: 42 | # Sphinx 1.2 43 | ref_context = self.env.temp_data 44 | modname = self.options.get( 45 | 'module', ref_context.get('py:module')) 46 | #print("*", modname, name_cls) 47 | if modname in self.env.config['modindex_exclude']: 48 | return None 49 | return org_PyObject_add_target_and_index(self, name_cls, sig, signode) 50 | 51 | 52 | def PyModule_run(self): 53 | env = self.state.document.settings.env 54 | modname = self.arguments[0].strip() 55 | excl = env.config['modindex_exclude'] 56 | if modname in excl: 57 | self.options['noindex'] = True 58 | EXCLUDES.setdefault(modname, []).append(env.docname) 59 | return org_PyModule_run(self) 60 | 61 | 62 | def setup(app): 63 | app.add_config_value('modindex_exclude', [], 'html') 64 | 65 | # global org_PythonModuleIndex_generate 66 | # org_PythonModuleIndex_generate = sphinx.domains.python.PythonModuleIndex.generate 67 | # sphinx.domains.python.PythonModuleIndex.generate = PythonModuleIndex_generate 68 | 69 | global org_PyObject_add_target_and_index 70 | org_PyObject_add_target_and_index = sphinx.domains.python.PyObject.add_target_and_index 71 | sphinx.domains.python.PyObject.add_target_and_index = PyObject_add_target_and_index 72 | 73 | global org_PyModule_run 74 | org_PyModule_run = sphinx.domains.python.PyModule.run 75 | sphinx.domains.python.PyModule.run = PyModule_run 76 | -------------------------------------------------------------------------------- /docs/sphinx_selective_exclude/search_auto_exclude.py: -------------------------------------------------------------------------------- 1 | # 2 | # This is a Sphinx documentation tool extension which allows to 3 | # automatically exclude from full-text search index document 4 | # which are not referenced via toctree::. It's intended to be 5 | # used with toctrees conditional on only:: directive, with the 6 | # idea being that if you didn't include it in the ToC, you don't 7 | # want the docs being findable by search either (for example, 8 | # because these docs contain information not pertinent to a 9 | # particular product configuration). 10 | # 11 | # This extension depends on "eager_only" extension and won't work 12 | # without it. 13 | # 14 | # Copyright (c) 2016 Paul Sokolovsky 15 | # Licensed under the terms of BSD license, see LICENSE file. 16 | # 17 | import sphinx 18 | 19 | 20 | org_StandaloneHTMLBuilder_index_page = None 21 | 22 | 23 | def StandaloneHTMLBuilder_index_page(self, pagename, doctree, title): 24 | if pagename not in self.env.files_to_rebuild: 25 | if pagename != self.env.config.master_doc and 'orphan' not in self.env.metadata[pagename]: 26 | print("Excluding %s from full-text index because it's not referenced in ToC" % pagename) 27 | return 28 | return org_StandaloneHTMLBuilder_index_page(self, pagename, doctree, title) 29 | 30 | 31 | def setup(app): 32 | global org_StandaloneHTMLBuilder_index_page 33 | org_StandaloneHTMLBuilder_index_page = sphinx.builders.html.StandaloneHTMLBuilder.index_page 34 | sphinx.builders.html.StandaloneHTMLBuilder.index_page = StandaloneHTMLBuilder_index_page 35 | -------------------------------------------------------------------------------- /docs/sphinxcontrib/cmtinc-buildhat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | sphinxcontrib.cmtinc 4 | ~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Extract comments from source files. 7 | 8 | See the README file for details. 9 | 10 | :author: Vilibald W. 11 | :license: MIT, see LICENSE for details 12 | """ 13 | 14 | import codecs 15 | import re 16 | from docutils import nodes 17 | from sphinx.util.nodes import nested_parse_with_titles 18 | from docutils.statemachine import ViewList 19 | from docutils.parsers.rst import Directive 20 | 21 | re_comment = re.compile("^\s?/\*\*\**(.*)$") 22 | re_cmtnext = re.compile("^[/\* | \*]?\**(.*)$") 23 | re_cmtend = re.compile("(.*)(\*/)+$") 24 | 25 | 26 | class ExtractError(Exception): 27 | pass 28 | 29 | 30 | class Extractor(object): 31 | """ 32 | Main extraction class 33 | """ 34 | 35 | def __init__(self): 36 | """ 37 | """ 38 | self.content = ViewList("", 'comment') 39 | self.lineno = 0 40 | self.is_multiline = False 41 | 42 | def extract(self, source, prefix): 43 | """ 44 | Process the source file and fill in the content. 45 | SOURCE is a fileobject. 46 | """ 47 | for l in source: 48 | self.lineno = self.lineno + 1 49 | l = l.strip() 50 | m = re_comment.match(l) 51 | 52 | if (m): 53 | self.comment(m.group(1), source, prefix) 54 | 55 | def comment(self, cur, source, prefix): 56 | """ 57 | Read the whole comment and strip the stars. 58 | 59 | CUR is currently read line and SOURCE is a fileobject 60 | with the source code. 61 | """ 62 | self.content.append(cur.strip(), "comment") 63 | 64 | for line in source: 65 | self.lineno = self.lineno + 1 66 | # line = line.strip() 67 | 68 | if re_cmtend.match(line): 69 | if (not self.is_multiline): 70 | break 71 | else: 72 | continue 73 | 74 | if line.startswith("/*"): 75 | if (not self.is_multiline): 76 | raise ExtractError( 77 | "%d: Nested comments are not supported yet." % 78 | self.lineno) 79 | else: 80 | continue 81 | 82 | if line.startswith(".. "): 83 | self.content.append(line, "comment") 84 | continue 85 | 86 | if line.startswith("\code"): 87 | self.is_multiline = True 88 | continue 89 | 90 | if line.startswith("\endcode"): 91 | self.is_multiline = False 92 | continue 93 | 94 | if line.startswith("/"): 95 | line = "/" + line 96 | 97 | m = re_cmtnext.match(line) 98 | if m: 99 | # self.content.append(" " + m.group(1).strip(), "comment") 100 | # self.content.append("" + m.group(1), "comment") 101 | self.content.append(prefix + m.group(1), "comment") 102 | continue 103 | 104 | self.content.append(line.strip(), "comment") 105 | 106 | self.content.append('\n', "comment") 107 | 108 | 109 | class CmtIncDirective(Directive): 110 | """ 111 | Directive to insert comments form source file. 112 | """ 113 | has_content = True 114 | required_arguments = 1 115 | optional_arguments = 1 116 | final_argument_whitespace = True 117 | option_spec = {} 118 | 119 | def run(self): 120 | """Run it """ 121 | self.reporter = self.state.document.reporter 122 | self.env = self.state.document.settings.env 123 | if self.arguments: 124 | if self.content: 125 | return [ 126 | self.reporter.warning( 127 | 'include-comment directive cannot have content only ' 128 | 'a filename argument', 129 | line=self.lineno) 130 | ] 131 | rel_filename, filename = self.env.relfn2path(self.arguments[0]) 132 | # prefix = '' if len(self.arguments) < 2 else re.sub('\'\"', '', self.arguments[1]) 133 | prefix = '' 134 | self.env.note_dependency(rel_filename) 135 | 136 | extr = Extractor() 137 | f = None 138 | try: 139 | encoding = self.options.get('encoding', 140 | self.env.config.source_encoding) 141 | codecinfo = codecs.lookup(encoding) 142 | f = codecs.StreamReaderWriter( 143 | open(filename, 'rb'), codecinfo[2], codecinfo[3], 'strict') 144 | extr.extract(f, prefix) 145 | except (IOError, OSError): 146 | return [ 147 | self.reporter.warning( 148 | 'Include file %r not found or reading it failed' % 149 | filename, 150 | line=self.lineno) 151 | ] 152 | except UnicodeError: 153 | return [ 154 | self.reporter.warning( 155 | 'Encoding %r used for reading included file %r seems to ' 156 | 'be wrong, try giving an :encoding: option' % 157 | (encoding, filename)) 158 | ] 159 | except ExtractError as e: 160 | return [ 161 | self.reporter.warning( 162 | 'Parsing error in %s : %s' % (filename, str(e)), 163 | line=self.lineno) 164 | ] 165 | finally: 166 | if f is not None: 167 | f.close() 168 | self.content = extr.content 169 | self.content_offset = 0 170 | # Create a node, to be populated by `nested_parse`. 171 | node = nodes.paragraph() 172 | # Parse the directive contents. 173 | nested_parse_with_titles(self.state, self.content, node) 174 | return node.children 175 | else: 176 | return [ 177 | self.reporter.warning( 178 | 'include-comment directive needs a filename argument', 179 | line=self.lineno) 180 | ] 181 | 182 | 183 | def setup(app): 184 | app.add_directive('include-comment', CmtIncDirective) 185 | 186 | 187 | if __name__ == '__main__': 188 | import sys 189 | 190 | if len(sys.argv) < 2: 191 | print("Usage: cmtinc.py ") 192 | exit(1) 193 | 194 | ext = Extractor() 195 | try: 196 | with open(sys.argv[1], 'r') as f: 197 | ext.extract(f,"") 198 | except ExtractError as e: 199 | print('Extraction error in external source file %r : %s' % 200 | (sys.argv[1], str(e))) 201 | for line in ext.content: 202 | print(line) 203 | -------------------------------------------------------------------------------- /docs/static/customstyle.css: -------------------------------------------------------------------------------- 1 | /* custom CSS for MicroPython docs 2 | */ 3 | 4 | .admonition-difference-to-cpython { 5 | border: 1px solid black; 6 | } 7 | 8 | .admonition-difference-to-cpython .admonition-title { 9 | margin: 4px; 10 | } 11 | -------------------------------------------------------------------------------- /docs/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaspberryPiFoundation/python-build-hat/78c0c6988f9c786113b12c4c6309aecccb36b1da/docs/static/favicon.ico -------------------------------------------------------------------------------- /docs/templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {% set css_files = css_files + ["_static/customstyle.css"] %} 3 | 4 | {# we change the master_doc variable so that links to the index 5 | page are to index.html instead of _index.html #} 6 | {% set master_doc = "index" %} 7 | -------------------------------------------------------------------------------- /docs/templates/replace.inc: -------------------------------------------------------------------------------- 1 | .. comment: This file is intended for global "replace" definitions. 2 | 3 | .. |see_cpython| replace:: See CPython documentation: 4 | -------------------------------------------------------------------------------- /docs/templates/topindex.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | 4 |

{{ _('Build HAT Documentation') }}

5 | 6 |

{{ _('Welcome! This is the documentation for the Raspberry Pi Build HAT') }}{{ release|e }}{% if last_updated %}, {{ _('last updated') }} {{ last_updated|e }}{% endif %}.

7 | 8 | 9 | 10 | 16 | 22 | 28 | 29 |
11 | 15 | 17 | 21 | 23 | 27 |
30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /docs/templates/versions.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Ports and Versions 4 | {{ port }} ({{ port_version }}) 5 | 6 | 7 |
8 |
9 |
Ports
10 | {% for slug, url in all_ports %} 11 |
{{ slug }}
12 | {% endfor %} 13 |
14 |
15 |
Versions
16 | {% for slug, url in all_versions %} 17 |
{{ slug }}
18 | {% endfor %} 19 |
20 |
21 |
Downloads
22 | {% for type, url in downloads %} 23 |
{{ type }}
24 | {% endfor %} 25 |
26 |
27 |
28 |
Links
29 |
30 | GitHub 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | flake8>=6.0.0 2 | flake8-bugbear>=22.10.27 3 | flake8-comprehensions>=3.10 4 | flake8-debugger 5 | flake8-docstrings>=1.6.0 6 | flake8-isort>=5.0 7 | flake8-pylint 8 | flake8-rst-docstrings 9 | flake8-string-format 10 | gpiozero 11 | pyserial 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """Setup file""" 4 | 5 | # Copyright (c) 2020-2021 Raspberry Pi Foundation 6 | # 7 | # SPDX-License-Identifier: MIT 8 | 9 | from setuptools import setup 10 | 11 | with open("README.md") as readme: 12 | long_description = readme.read() 13 | 14 | with open('VERSION') as versionf: 15 | version = versionf.read().strip() 16 | 17 | setup(name='buildhat', 18 | version=version, 19 | description='Build HAT Python library', 20 | license='MIT', 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | author='Raspberry Pi Foundation', 24 | author_email='web@raspberrypi.org', 25 | url='https://github.com/RaspberryPiFoundation/python-build-hat', 26 | project_urls={'Bug Tracker': "https://github.com/RaspberryPiFoundation/python-build-hat/issues"}, 27 | packages=['buildhat'], 28 | package_data={ 29 | "": ["data/firmware.bin", "data/signature.bin", "data/version"], 30 | }, 31 | python_requires='>=3.7', 32 | install_requires=['gpiozero', 'pyserial']) 33 | -------------------------------------------------------------------------------- /test/color.py: -------------------------------------------------------------------------------- 1 | """Test Color Sensor functionality""" 2 | import time 3 | import unittest 4 | 5 | from buildhat import ColorSensor 6 | 7 | 8 | class TestColor(unittest.TestCase): 9 | """Test color sensor functions""" 10 | 11 | def test_color_interval(self): 12 | """Test color sensor interval""" 13 | color = ColorSensor('A') 14 | color.avg_reads = 1 15 | color.interval = 10 16 | count = 1000 17 | expected_dur = count * color.interval * 1e-3 18 | 19 | start = time.time() 20 | for _ in range(count): 21 | color.get_ambient_light() 22 | end = time.time() 23 | diff = abs((end - start) - expected_dur) 24 | self.assertLess(diff, 0.25) 25 | 26 | start = time.time() 27 | for _ in range(count): 28 | color.get_color_rgbi() 29 | end = time.time() 30 | diff = abs((end - start) - expected_dur) 31 | self.assertLess(diff, 0.25) 32 | 33 | def test_caching(self): 34 | """Test to make sure we're not reading cached data""" 35 | color = ColorSensor('A') 36 | color.avg_reads = 1 37 | color.interval = 1 38 | 39 | for _ in range(100): 40 | color.mode(2) 41 | self.assertEqual(len(color.get()), 1) 42 | color.mode(5) 43 | self.assertEqual(len(color.get()), 4) 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /test/distance.py: -------------------------------------------------------------------------------- 1 | """Test distance sensor""" 2 | 3 | import unittest 4 | 5 | from buildhat import DistanceSensor 6 | from buildhat.exc import DeviceError 7 | 8 | 9 | class TestDistance(unittest.TestCase): 10 | """Test distance sensor""" 11 | 12 | def test_properties(self): 13 | """Test properties of sensor""" 14 | d = DistanceSensor('A') 15 | self.assertIsInstance(d.distance, int) 16 | self.assertIsInstance(d.threshold_distance, int) 17 | 18 | def test_distance(self): 19 | """Test obtaining distance""" 20 | d = DistanceSensor('A') 21 | self.assertIsInstance(d.get_distance(), int) 22 | 23 | def test_eyes(self): 24 | """Test lighting LEDs on sensor""" 25 | d = DistanceSensor('A') 26 | d.eyes(100, 100, 100, 100) 27 | 28 | def test_duplicate_port(self): 29 | """Test using same port""" 30 | d = DistanceSensor('A') # noqa: F841 31 | self.assertRaises(DeviceError, DistanceSensor, 'A') 32 | 33 | def test_del(self): 34 | """Test deleting sensor""" 35 | d = DistanceSensor('A') 36 | del d 37 | DistanceSensor('A') 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /test/hat.py: -------------------------------------------------------------------------------- 1 | """Test hat functionality""" 2 | 3 | import unittest 4 | 5 | from buildhat import Hat 6 | 7 | 8 | class TestHat(unittest.TestCase): 9 | """Test hat functions""" 10 | 11 | def test_vin(self): 12 | """Test voltage measure function""" 13 | h = Hat() 14 | vin = h.get_vin() 15 | self.assertGreaterEqual(vin, 7.2) 16 | self.assertLessEqual(vin, 8.5) 17 | 18 | def test_get(self): 19 | """Test getting list of devices""" 20 | h = Hat() 21 | self.assertIsInstance(h.get(), dict) 22 | 23 | def test_serial(self): 24 | """Test setting serial device""" 25 | Hat(device="/dev/serial0") 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /test/light.py: -------------------------------------------------------------------------------- 1 | """Test light functionality""" 2 | 3 | import time 4 | import unittest 5 | 6 | from buildhat import Light 7 | from buildhat.exc import LightError 8 | 9 | 10 | class TestLight(unittest.TestCase): 11 | """Test light functions""" 12 | 13 | def test_light(self): 14 | """Test light functions""" 15 | light = Light('A') 16 | light.on() 17 | light.brightness(0) 18 | light.brightness(100) 19 | time.sleep(1) 20 | light.brightness(25) 21 | time.sleep(1) 22 | self.assertRaises(LightError, light.brightness, -1) 23 | self.assertRaises(LightError, light.brightness, 101) 24 | light.off() 25 | 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /test/matrix.py: -------------------------------------------------------------------------------- 1 | """Test matrix functionality""" 2 | 3 | import time 4 | import unittest 5 | 6 | from buildhat import Matrix 7 | from buildhat.exc import MatrixError 8 | 9 | 10 | class TestMatrix(unittest.TestCase): 11 | """Test matrix functions""" 12 | 13 | def test_matrix(self): 14 | """Test setting matrix pixels""" 15 | matrix = Matrix('A') 16 | matrix.set_pixels([[(10, 10) for x in range(3)] for y in range(3)]) 17 | time.sleep(1) 18 | self.assertRaises(MatrixError, matrix.set_pixels, [[(10, 10) for x in range(3)] for y in range(4)]) 19 | self.assertRaises(MatrixError, matrix.set_pixels, [[(10, 10) for x in range(4)] for y in range(3)]) 20 | self.assertRaises(MatrixError, matrix.set_pixels, [[(11, 10) for x in range(3)] for y in range(3)]) 21 | self.assertRaises(MatrixError, matrix.set_pixels, [[(-1, 10) for x in range(3)] for y in range(3)]) 22 | self.assertRaises(MatrixError, matrix.set_pixels, [[(10, 11) for x in range(3)] for y in range(3)]) 23 | self.assertRaises(MatrixError, matrix.set_pixels, [[(10, -1) for x in range(3)] for y in range(3)]) 24 | self.assertRaises(MatrixError, matrix.set_pixels, [[("gold", 10) for x in range(3)] for y in range(3)]) 25 | self.assertRaises(MatrixError, matrix.set_pixels, [[(10, "test") for x in range(3)] for y in range(3)]) 26 | matrix.set_pixels([[("pink", 10) for x in range(3)] for y in range(3)]) 27 | time.sleep(1) 28 | 29 | def test_clear(self): 30 | """Test clearing matrix""" 31 | matrix = Matrix('A') 32 | matrix.clear() 33 | matrix.clear((10, 10)) 34 | time.sleep(1) 35 | matrix.clear(("yellow", 10)) 36 | time.sleep(1) 37 | self.assertRaises(MatrixError, matrix.clear, ("gold", 10)) 38 | self.assertRaises(MatrixError, matrix.clear, (10, -1)) 39 | self.assertRaises(MatrixError, matrix.clear, (10, 11)) 40 | self.assertRaises(MatrixError, matrix.clear, (-1, 10)) 41 | self.assertRaises(MatrixError, matrix.clear, (10, 11)) 42 | 43 | def test_transition(self): 44 | """Test transitions""" 45 | matrix = Matrix('A') 46 | matrix.clear(("green", 10)) 47 | time.sleep(1) 48 | matrix.set_transition(1) 49 | matrix.set_pixels([[("blue", 10) for x in range(3)] for y in range(3)]) 50 | time.sleep(4) 51 | matrix.set_transition(2) 52 | matrix.set_pixels([[("red", 10) for x in range(3)] for y in range(3)]) 53 | time.sleep(4) 54 | matrix.set_transition(0) 55 | self.assertRaises(MatrixError, matrix.set_transition, -1) 56 | self.assertRaises(MatrixError, matrix.set_transition, 3) 57 | self.assertRaises(MatrixError, matrix.set_transition, "test") 58 | 59 | def test_level(self): 60 | """Test level""" 61 | matrix = Matrix('A') 62 | matrix.clear(("orange", 10)) 63 | time.sleep(1) 64 | matrix.level(5) 65 | time.sleep(4) 66 | self.assertRaises(MatrixError, matrix.level, -1) 67 | self.assertRaises(MatrixError, matrix.level, 10) 68 | self.assertRaises(MatrixError, matrix.level, "test") 69 | 70 | def test_pixel(self): 71 | """Test pixel""" 72 | matrix = Matrix('A') 73 | matrix.clear() 74 | matrix.set_pixel((0, 0), ("red", 10)) 75 | matrix.set_pixel((2, 2), ("red", 10)) 76 | time.sleep(1) 77 | self.assertRaises(MatrixError, matrix.set_pixel, (-1, 0), ("red", 10)) 78 | self.assertRaises(MatrixError, matrix.set_pixel, (0, -1), ("red", 10)) 79 | self.assertRaises(MatrixError, matrix.set_pixel, (3, 0), ("red", 10)) 80 | self.assertRaises(MatrixError, matrix.set_pixel, (0, 3), ("red", 10)) 81 | self.assertRaises(MatrixError, matrix.set_pixel, (0, 0), ("gold", 10)) 82 | self.assertRaises(MatrixError, matrix.set_pixel, (0, 0), ("red", -1)) 83 | self.assertRaises(MatrixError, matrix.set_pixel, (0, 0), ("red", 11)) 84 | 85 | 86 | if __name__ == '__main__': 87 | unittest.main() 88 | -------------------------------------------------------------------------------- /test/motors.py: -------------------------------------------------------------------------------- 1 | """Test motors""" 2 | 3 | import time 4 | import unittest 5 | 6 | from buildhat import Hat, Motor 7 | from buildhat.exc import DeviceError, MotorError 8 | 9 | 10 | class TestMotor(unittest.TestCase): 11 | """Test motors""" 12 | 13 | THRESHOLD_DISTANCE = 15 14 | 15 | def test_rotations(self): 16 | """Test motor rotating""" 17 | m = Motor('A') 18 | pos1 = m.get_position() 19 | m.run_for_rotations(2) 20 | pos2 = m.get_position() 21 | rotated = (pos2 - pos1) / 360 22 | self.assertLess(abs(rotated - 2), 0.5) 23 | 24 | def test_nonblocking(self): 25 | """Test motor nonblocking mode""" 26 | m = Motor('A') 27 | m.set_default_speed(10) 28 | last = 0 29 | for delay in [1, 0]: 30 | for _ in range(3): 31 | m.run_to_position(90, blocking=False) 32 | time.sleep(delay) 33 | m.run_to_position(90, blocking=False) 34 | time.sleep(delay) 35 | m.run_to_position(90, blocking=False) 36 | time.sleep(delay) 37 | m.run_to_position(last, blocking=False) 38 | time.sleep(delay) 39 | # Wait for a bit, before reading last position 40 | time.sleep(7) 41 | pos1 = m.get_aposition() 42 | diff = abs((last - pos1 + 180) % 360 - 180) 43 | self.assertLess(diff, self.THRESHOLD_DISTANCE) 44 | 45 | def test_nonblocking_multiple(self): 46 | """Test motor nonblocking mode""" 47 | m1 = Motor('A') 48 | m1.set_default_speed(10) 49 | m2 = Motor('B') 50 | m2.set_default_speed(10) 51 | last = 0 52 | for delay in [1, 0]: 53 | for _ in range(3): 54 | m1.run_to_position(90, blocking=False) 55 | m2.run_to_position(90, blocking=False) 56 | time.sleep(delay) 57 | m1.run_to_position(90, blocking=False) 58 | m2.run_to_position(90, blocking=False) 59 | time.sleep(delay) 60 | m1.run_to_position(90, blocking=False) 61 | m2.run_to_position(90, blocking=False) 62 | time.sleep(delay) 63 | m1.run_to_position(last, blocking=False) 64 | m2.run_to_position(last, blocking=False) 65 | time.sleep(delay) 66 | # Wait for a bit, before reading last position 67 | time.sleep(7) 68 | pos1 = m1.get_aposition() 69 | diff = abs((last - pos1 + 180) % 360 - 180) 70 | self.assertLess(diff, self.THRESHOLD_DISTANCE) 71 | pos2 = m2.get_aposition() 72 | diff = abs((last - pos2 + 180) % 360 - 180) 73 | self.assertLess(diff, self.THRESHOLD_DISTANCE) 74 | 75 | def test_nonblocking_mixed(self): 76 | """Test motor nonblocking mode mixed with blocking mode""" 77 | m = Motor('A') 78 | m.run_for_seconds(5, blocking=False) 79 | m.run_for_degrees(360) 80 | m.run_for_seconds(5, blocking=False) 81 | m.run_to_position(180) 82 | m.run_for_seconds(5, blocking=False) 83 | m.run_for_seconds(5) 84 | m.run_for_seconds(5, blocking=False) 85 | m.start() 86 | m.run_for_seconds(5, blocking=False) 87 | m.stop() 88 | 89 | def test_position(self): 90 | """Test motor goes to desired position""" 91 | m = Motor('A') 92 | m.run_to_position(0) 93 | pos1 = m.get_aposition() 94 | diff = abs((0 - pos1 + 180) % 360 - 180) 95 | self.assertLess(diff, self.THRESHOLD_DISTANCE) 96 | 97 | m.run_to_position(180) 98 | pos1 = m.get_aposition() 99 | diff = abs((180 - pos1 + 180) % 360 - 180) 100 | self.assertLess(diff, self.THRESHOLD_DISTANCE) 101 | 102 | def test_time(self): 103 | """Test motor runs for correct duration""" 104 | m = Motor('A') 105 | t1 = time.time() 106 | m.run_for_seconds(5) 107 | t2 = time.time() 108 | self.assertEqual(int(t2 - t1), 5) 109 | 110 | def test_speed(self): 111 | """Test setting motor speed""" 112 | m = Motor('A') 113 | m.set_default_speed(50) 114 | self.assertRaises(MotorError, m.set_default_speed, -101) 115 | self.assertRaises(MotorError, m.set_default_speed, 101) 116 | 117 | def test_plimit(self): 118 | """Test altering power limit of motor""" 119 | m = Motor('A') 120 | m.plimit(0.5) 121 | self.assertRaises(MotorError, m.plimit, -1) 122 | self.assertRaises(MotorError, m.plimit, 2) 123 | 124 | def test_pwm(self): 125 | """Test PWMing motor""" 126 | m = Motor('A') 127 | m.pwm(0.3) 128 | time.sleep(0.5) 129 | m.pwm(0) 130 | self.assertRaises(MotorError, m.pwm, -2) 131 | self.assertRaises(MotorError, m.pwm, 2) 132 | 133 | def test_callback(self): 134 | """Test setting callback""" 135 | m = Motor('A') 136 | 137 | def handle_motor(speed, pos, apos): 138 | handle_motor.evt += 1 139 | handle_motor.evt = 0 140 | m.when_rotated = handle_motor 141 | m.run_for_seconds(1) 142 | self.assertGreater(handle_motor.evt, 0) 143 | 144 | def test_callback_interval(self): 145 | """Test setting callback and interval""" 146 | m = Motor('A') 147 | m.interval = 10 148 | 149 | def handle_motor(speed, pos, apos): 150 | handle_motor.evt += 1 151 | handle_motor.evt = 0 152 | m.when_rotated = handle_motor 153 | m.run_for_seconds(5) 154 | self.assertGreater(handle_motor.evt, 0.8 * ((1 / ((m.interval) * 1e-3)) * 5)) 155 | 156 | def test_none_callback(self): 157 | """Test setting empty callback""" 158 | m = Motor('A') 159 | m.when_rotated = None 160 | m.start() 161 | time.sleep(0.5) 162 | m.stop() 163 | 164 | def test_duplicate_port(self): 165 | """Test using same port for motor""" 166 | m1 = Motor('A') # noqa: F841 167 | self.assertRaises(DeviceError, Motor, 'A') 168 | 169 | def test_del(self): 170 | """Test deleting motor""" 171 | m1 = Motor('A') 172 | del m1 173 | Motor('A') 174 | 175 | def test_continuous_start(self): 176 | """Test starting motor for 5mins""" 177 | t = time.time() + (60 * 5) 178 | m = Motor('A') 179 | toggle = 0 180 | while time.time() < t: 181 | m.start(toggle) 182 | toggle ^= 1 183 | 184 | def test_continuous_degrees(self): 185 | """Test setting degrees for 5mins""" 186 | t = time.time() + (60 * 5) 187 | m = Motor('A') 188 | toggle = 0 189 | while time.time() < t: 190 | m.run_for_degrees(toggle) 191 | toggle ^= 1 192 | 193 | def test_continuous_position(self): 194 | """Test setting position of motor for 5mins""" 195 | t = time.time() + (60 * 5) 196 | m = Motor('A') 197 | toggle = 0 198 | while time.time() < t: 199 | m.run_to_position(toggle) 200 | toggle ^= 1 201 | 202 | def test_continuous_feedback(self): 203 | """Test feedback of motor for 30mins""" 204 | Hat(debug=True) 205 | t = time.time() + (60 * 30) 206 | m = Motor('A') 207 | m.start(40) 208 | while time.time() < t: 209 | _ = (m.get_speed(), m.get_position(), m.get_aposition()) 210 | 211 | def test_interval(self): 212 | """Test motor interval""" 213 | m = Motor('A') 214 | m.interval = 10 215 | count = 1000 216 | expected_dur = count * m.interval * 1e-3 217 | start = time.time() 218 | for _ in range(count): 219 | m.get_position() 220 | end = time.time() 221 | diff = abs((end - start) - expected_dur) 222 | self.assertLess(diff, expected_dur * 0.1) 223 | 224 | def test_dual_interval(self): 225 | """Test dual motor interval""" 226 | m1 = Motor('A') 227 | m2 = Motor('B') 228 | for interval in [20, 10]: 229 | m1.interval = interval 230 | m2.interval = interval 231 | count = 1000 232 | expected_dur = count * m1.interval * 1e-3 233 | start = time.time() 234 | for _ in range(count): 235 | m1.get_position() 236 | m2.get_position() 237 | end = time.time() 238 | diff = abs((end - start) - expected_dur) 239 | self.assertLess(diff, expected_dur * 0.1) 240 | 241 | 242 | if __name__ == '__main__': 243 | unittest.main() 244 | -------------------------------------------------------------------------------- /test/wedo.py: -------------------------------------------------------------------------------- 1 | """Test wedo sensor functionality""" 2 | 3 | import unittest 4 | 5 | from buildhat import MotionSensor, TiltSensor 6 | 7 | 8 | class TestWeDo(unittest.TestCase): 9 | """Test wedo sensor functions""" 10 | 11 | def test_motionsensor(self): 12 | """Test motion sensor""" 13 | motion = MotionSensor('A') 14 | dist = motion.get_distance() 15 | self.assertIsInstance(dist, int) 16 | 17 | def test_tiltsensor(self): 18 | """Test tilt sensor 19 | 20 | ToDo - Test when I re-find this sensor 21 | """ 22 | tilt = TiltSensor('B') 23 | tvalue = tilt.get_tilt() 24 | self.assertIsInstance(tvalue, tuple) 25 | self.assertIsInstance(tvalue[0], int) 26 | self.assertIsInstance(tvalue[1], int) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | --------------------------------------------------------------------------------