├── .github └── workflows │ ├── build.yml │ ├── qa.yml │ └── test.yml ├── .gitignore ├── .stickler.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── as7262 ├── __init__.py └── __main__.py ├── check.sh ├── examples ├── bargraph.py └── spectrum.py ├── install.sh ├── library └── .coveragerc ├── pyproject.toml ├── requirements-dev.txt ├── tests ├── __init__.py ├── conftest.py ├── test_features.py ├── test_setup.py └── tools.py ├── tox.ini └── uninstall.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Python ${{ matrix.python }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: ['3.9', '3.10', '3.11'] 16 | 17 | env: 18 | RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} 19 | 20 | steps: 21 | - name: Checkout Code 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Python ${{ matrix.python }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python }} 28 | 29 | - name: Install Dependencies 30 | run: | 31 | make dev-deps 32 | 33 | - name: Build Packages 34 | run: | 35 | make build 36 | 37 | - name: Upload Packages 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: ${{ env.RELEASE_FILE }} 41 | path: dist/ 42 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: linting & spelling 12 | runs-on: ubuntu-latest 13 | env: 14 | TERM: xterm-256color 15 | 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python '3,11' 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.11' 24 | 25 | - name: Install Dependencies 26 | run: | 27 | make dev-deps 28 | 29 | - name: Run Quality Assurance 30 | run: | 31 | make qa 32 | 33 | - name: Run Code Checks 34 | run: | 35 | make check 36 | 37 | - name: Run Bash Code Checks 38 | run: | 39 | make shellcheck 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Python ${{ matrix.python }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: ['3.9', '3.10', '3.11'] 16 | 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Python ${{ matrix.python }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python }} 25 | 26 | - name: Install Dependencies 27 | run: | 28 | make dev-deps 29 | 30 | - name: Run Tests 31 | run: | 32 | make pytest 33 | 34 | - name: Coverage 35 | if: ${{ matrix.python == '3.9' }} 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: | 39 | python -m pip install coveralls 40 | coveralls --service=github 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | _build/ 3 | *.o 4 | *.so 5 | *.a 6 | *.py[cod] 7 | *.egg-info 8 | dist/ 9 | __pycache__ 10 | .DS_Store 11 | *.deb 12 | *.dsc 13 | *.build 14 | *.changes 15 | *.orig.* 16 | packaging/*tar.xz 17 | library/debian/ 18 | .coverage 19 | .pytest_cache 20 | .tox 21 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | flake8: 4 | python: 3 5 | max-line-length: 160 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | ----- 3 | 4 | * Repackage to hatch/pyproject.toml 5 | * Migrate to smbus2 6 | 7 | 0.1.0 8 | ----- 9 | 10 | * Port to new i2cdevice API 11 | * Breaking change from singleton module to AS7262 class 12 | 13 | 0.0.2 14 | ----- 15 | 16 | * Extended reset delay to avoid IO Errors 17 | 18 | 0.0.1 19 | ----- 20 | 21 | * Initial Release 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) 2 | LIBRARY_VERSION := $(shell hatch version 2> /dev/null) 3 | 4 | .PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy 5 | usage: 6 | ifdef LIBRARY_NAME 7 | @echo "Library: ${LIBRARY_NAME}" 8 | @echo "Version: ${LIBRARY_VERSION}\n" 9 | else 10 | @echo "WARNING: You should 'make dev-deps'\n" 11 | endif 12 | @echo "Usage: make , where target is one of:\n" 13 | @echo "install: install the library locally from source" 14 | @echo "uninstall: uninstall the local library" 15 | @echo "dev-deps: install Python dev dependencies" 16 | @echo "check: perform basic integrity checks on the codebase" 17 | @echo "qa: run linting and package QA" 18 | @echo "pytest: run Python test fixtures" 19 | @echo "clean: clean Python build and dist directories" 20 | @echo "build: build Python distribution files" 21 | @echo "testdeploy: build and upload to test PyPi" 22 | @echo "deploy: build and upload to PyPi" 23 | @echo "tag: tag the repository with the current version\n" 24 | 25 | version: 26 | @hatch version 27 | 28 | install: 29 | ./install.sh --unstable 30 | 31 | uninstall: 32 | ./uninstall.sh 33 | 34 | dev-deps: 35 | python3 -m pip install -r requirements-dev.txt 36 | sudo apt install dos2unix shellcheck 37 | 38 | check: 39 | @bash check.sh 40 | 41 | shellcheck: 42 | shellcheck *.sh 43 | 44 | qa: 45 | tox -e qa 46 | 47 | pytest: 48 | tox -e py 49 | 50 | nopost: 51 | @bash check.sh --nopost 52 | 53 | tag: version 54 | git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" 55 | 56 | build: check 57 | @hatch build 58 | 59 | clean: 60 | -rm -r dist 61 | 62 | testdeploy: build 63 | twine upload --repository testpypi dist/* 64 | 65 | deploy: nopost build 66 | twine upload dist/* 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AS7262 Spectral Sensor 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/as7262-python/test.yml?branch=main)](https://github.com/pimoroni/as7262-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/as7262-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/as7262-python?branch=main) 5 | [![PyPi Package](https://img.shields.io/pypi/v/as7262.svg)](https://pypi.python.org/pypi/as7262) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/as7262.svg)](https://pypi.python.org/pypi/as7262) 7 | 8 | 9 | Suitable for detecting the properties of ambient light, light passing through a liquid or light reflected from an object the AS7262 spectral sensor has 6 spectral channels at 450 (violet), 500 (blue), 550 (green), 570 (yellow), 600 (orange) and 650nm (red). 10 | 11 | ## Installing 12 | 13 | ### Full install (recommended): 14 | 15 | We've created an easy installation script that will install all pre-requisites and get your AS7262 16 | up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal 17 | on your Raspberry Pi desktop, as illustrated below: 18 | 19 | ![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) 20 | 21 | In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: 22 | 23 | ```bash 24 | git clone https://github.com/pimoroni/as7262-python 25 | cd as7262-python 26 | ./install.sh 27 | ``` 28 | 29 | **Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: 30 | 31 | ``` 32 | source ~/.virtualenvs/pimoroni/bin/activate 33 | ``` 34 | 35 | ### Development: 36 | 37 | If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: 38 | 39 | ```bash 40 | git clone https://github.com/pimoroni/as7262-python 41 | cd as7262-python 42 | ./install.sh --unstable 43 | ``` 44 | 45 | The install script should do it for you, but in some cases you might have to enable the i2c bus. 46 | 47 | On a Raspberry Pi you can do that like so: 48 | 49 | ``` 50 | sudo raspi-config nonint do_i2c 0 51 | ``` -------------------------------------------------------------------------------- /as7262/__init__.py: -------------------------------------------------------------------------------- 1 | """Library for the AS7262 Visible Light Spectral Sensor.""" 2 | import struct 3 | import time 4 | 5 | from i2cdevice import BitField, Device, Register, _int_to_bytes 6 | from i2cdevice.adapter import Adapter, LookupAdapter 7 | 8 | __version__ = '1.0.0' 9 | 10 | 11 | class as7262VirtualRegisterBus(): 12 | """AS7262 Virtual Register. 13 | 14 | This class implements the wacky virtual register setup 15 | of the AS7262 and allows i2cdevice.Device to "just work" 16 | without having to worry about how registers are actually 17 | read or written under the hood. 18 | 19 | """ 20 | 21 | def __init__(self, i2c_dev=None): 22 | """Initialise virtual register class. 23 | 24 | :param bus: SMBus bus ID 25 | 26 | """ 27 | if i2c_dev is None: 28 | import smbus2 29 | self._i2c_bus = smbus2.SMBus(1) 30 | else: 31 | self._i2c_bus = i2c_dev 32 | 33 | def get_status(self, address): 34 | """Return the AS7262 status register.""" 35 | return self._i2c_bus.read_byte_data(address, 0x00) 36 | 37 | def write_i2c_block_data(self, address, register, values): 38 | """Right one or more values to AS7262 virtual registers.""" 39 | for offset in range(len(values)): 40 | while True: 41 | if (self.get_status(address) & 0b10) == 0: 42 | break 43 | self._i2c_bus.write_byte_data(address, 0x01, register | 0x80) 44 | while True: 45 | if (self.get_status(address) & 0b10) == 0: 46 | break 47 | self._i2c_bus.write_byte_data(address, 0x01, values[offset]) 48 | 49 | def read_i2c_block_data(self, address, register, length): 50 | """Read one or more values from AS7262 virtual registers.""" 51 | result = [] 52 | for offset in range(length): 53 | while True: 54 | if (self.get_status(address) & 0b10) == 0: 55 | break 56 | self._i2c_bus.write_byte_data(address, 0x01, register + offset) 57 | while True: 58 | if (self.get_status(address) & 0b01) == 1: 59 | break 60 | result.append(self._i2c_bus.read_byte_data(address, 0x02)) 61 | return result 62 | 63 | 64 | class FWVersionAdapter(Adapter): 65 | """Convert the AS7262 firmware version number to a human-readable string.""" 66 | 67 | def _decode(self, value): 68 | major_version = (value & 0x00F0) >> 4 69 | minor_version = ((value & 0x000F) << 2) | ((value & 0b1100000000000000) >> 14) 70 | sub_version = (value & 0b0011111100000000) >> 8 71 | return '{}.{}.{}'.format(major_version, minor_version, sub_version) 72 | 73 | 74 | class FloatAdapter(Adapter): 75 | """Convert a 4 byte register set into a float.""" 76 | 77 | def _decode(self, value): 78 | b = _int_to_bytes(value, 4) 79 | return struct.unpack('>f', bytearray(b))[0] 80 | 81 | 82 | class IntegrationTimeAdapter(Adapter): 83 | """Scale integration time in ms to LSBs.""" 84 | 85 | def _decode(self, value): 86 | return value / 2.8 87 | 88 | def _encode(self, value): 89 | return int(value * 2.8) & 0xff 90 | 91 | 92 | class CalibratedValues: 93 | """Store the 6 band spectral values.""" 94 | 95 | def __init__(self, red, orange, yellow, green, blue, violet): # noqa D107 96 | self.red = red 97 | self.orange = orange 98 | self.yellow = yellow 99 | self.green = green 100 | self.blue = blue 101 | self.violet = violet 102 | 103 | def __iter__(self): # noqa D107 104 | for colour in ['red', 'orange', 'yellow', 'green', 'blue', 'violet']: 105 | yield getattr(self, colour) 106 | 107 | 108 | class AS7262: 109 | def __init__(self, i2c_dev=None): 110 | self._as7262 = Device(0x49, i2c_dev=as7262VirtualRegisterBus(i2c_dev=i2c_dev), bit_width=8, registers=( 111 | Register('VERSION', 0x00, fields=( 112 | BitField('hw_type', 0xFF000000), 113 | BitField('hw_version', 0x00FF0000), 114 | BitField('fw_version', 0x0000FFFF, adapter=FWVersionAdapter()), 115 | ), bit_width=32, read_only=True), 116 | Register('CONTROL', 0x04, fields=( 117 | BitField('reset', 0b10000000), 118 | BitField('interrupt', 0b01000000), 119 | BitField('gain_x', 0b00110000, adapter=LookupAdapter({ 120 | 1: 0b00, 3.7: 0b01, 16: 0b10, 64: 0b11 121 | })), 122 | BitField('measurement_mode', 0b00001100), 123 | BitField('data_ready', 0b00000010), 124 | )), 125 | Register('INTEGRATION_TIME', 0x05, fields=( 126 | BitField('ms', 0xFF, adapter=IntegrationTimeAdapter()), 127 | )), 128 | Register('TEMPERATURE', 0x06, fields=( 129 | BitField('degrees_c', 0xFF), 130 | )), 131 | Register('LED_CONTROL', 0x07, fields=( 132 | BitField('illumination_current_limit_ma', 0b00110000, adapter=LookupAdapter({ 133 | 12.5: 0b00, 25: 0b01, 50: 0b10, 100: 0b11 134 | })), 135 | BitField('illumination_enable', 0b00001000), 136 | BitField('indicator_current_limit_ma', 0b00000110, adapter=LookupAdapter({ 137 | 1: 0b00, 2: 0b01, 4: 0b10, 8: 0b11 138 | })), 139 | BitField('indicator_enable', 0b00000001), 140 | )), 141 | Register('DATA', 0x08, fields=( 142 | BitField('v', 0xFFFF00000000000000000000), 143 | BitField('b', 0x0000FFFF0000000000000000), 144 | BitField('g', 0x00000000FFFF000000000000), 145 | BitField('y', 0x000000000000FFFF00000000), 146 | BitField('o', 0x0000000000000000FFFF0000), 147 | BitField('r', 0x00000000000000000000FFFF), 148 | ), bit_width=96), 149 | Register('CALIBRATED_DATA', 0x14, fields=( 150 | BitField('v', 0xFFFFFFFF << (32 * 5), adapter=FloatAdapter()), 151 | BitField('b', 0xFFFFFFFF << (32 * 4), adapter=FloatAdapter()), 152 | BitField('g', 0xFFFFFFFF << (32 * 3), adapter=FloatAdapter()), 153 | BitField('y', 0xFFFFFFFF << (32 * 2), adapter=FloatAdapter()), 154 | BitField('o', 0xFFFFFFFF << (32 * 1), adapter=FloatAdapter()), 155 | BitField('r', 0xFFFFFFFF << (32 * 0), adapter=FloatAdapter()), 156 | ), bit_width=192), 157 | )) 158 | 159 | # TODO : Integrate into i2cdevice so that LookupAdapter fields can always be exported to constants 160 | # Iterate through all register fields and export their lookup tables to constants 161 | for register in self._as7262.registers: 162 | register = self._as7262.registers[register] 163 | for field in register.fields: 164 | field = register.fields[field] 165 | if isinstance(field.adapter, LookupAdapter): 166 | for key in field.adapter.lookup_table: 167 | value = field.adapter.lookup_table[key] 168 | name = 'AS7262_{register}_{field}_{key}'.format( 169 | register=register.name, 170 | field=field.name, 171 | key=key 172 | ).upper() 173 | locals()[name] = key 174 | 175 | self.soft_reset() 176 | 177 | def soft_reset(self): 178 | """Set the soft reset register bit of the AS7262.""" 179 | self._as7262.set('CONTROL', reset=1) 180 | # Polling for the state of the reset flag does not work here 181 | # since the fragile virtual register state machine cannot 182 | # respond while in a soft reset condition 183 | # So, just wait long enough for it to reset fully... 184 | time.sleep(2.0) 185 | 186 | def get_calibrated_values(self, timeout=10): 187 | """Return an instance of CalibratedValues containing the 6 spectral bands.""" 188 | t_start = time.time() 189 | while self._as7262.get('CONTROL').data_ready == 0 and (time.time() - t_start) <= timeout: 190 | pass 191 | data = self._as7262.get('CALIBRATED_DATA') 192 | return CalibratedValues(data.r, data.o, data.y, data.g, data.b, data.v) 193 | 194 | def set_gain(self, gain): 195 | """Set the gain amount of the AS7262. 196 | 197 | :param gain: gain multiplier, one of 1, 3.7, 16 or 64 198 | 199 | """ 200 | self._as7262.set('CONTROL', gain_x=gain) 201 | 202 | def set_measurement_mode(self, mode): 203 | """Set the AS7262 measurement mode. 204 | 205 | :param mode: 0-3 206 | 207 | """ 208 | self._as7262.set('CONTROL', measurement_mode=mode) 209 | 210 | def set_integration_time(self, time_ms): 211 | """Set the AS7262 sensor integration time in milliseconds. 212 | 213 | :param time_ms: Time in milliseconds from 0 to ~91 214 | 215 | """ 216 | self._as7262.set('INTEGRATION_TIME', ms=time_ms) 217 | 218 | def set_illumination_led_current(self, current): 219 | """Set the AS7262 illumination LED current in milliamps. 220 | 221 | :param current: Value in milliamps, one of 12.5, 25, 50 or 100 222 | 223 | """ 224 | self._as7262.set('LED_CONTROL', illumination_current_limit_ma=current) 225 | 226 | def set_indicator_led_current(self, current): 227 | """Set the AS7262 indicator LED current in milliamps. 228 | 229 | :param current: Value in milliamps, one of 1, 2, 4 or 8 230 | 231 | """ 232 | self._as7262.set('LED_CONTROL', indicator_current_limit_ma=current) 233 | 234 | def set_illumination_led(self, state): 235 | """Set the AS7262 illumination LED state. 236 | 237 | :param state: True = On, False = Off 238 | 239 | """ 240 | self._as7262.set('LED_CONTROL', illumination_enable=state) 241 | 242 | def set_indicator_led(self, state): 243 | """Set the AS7262 indicator LED state. 244 | 245 | :param state: True = On, False = Off 246 | 247 | """ 248 | self._as7262.set('LED_CONTROL', indicator_enable=state) 249 | 250 | def get_version(self): 251 | """Get the hardware type, version and firmware version from the AS7262.""" 252 | version = self._as7262.get('VERSION') 253 | return version.hw_type, version.hw_version, version.fw_version 254 | -------------------------------------------------------------------------------- /as7262/__main__.py: -------------------------------------------------------------------------------- 1 | """Library for the AS7262 Viisble Light Spectral Sensor.""" 2 | import as7262 3 | 4 | if __name__ == '__main__': 5 | as7262.soft_reset() 6 | 7 | hw_type, hw_version, fw_version = as7262.get_version() 8 | 9 | print('{}'.format(fw_version)) 10 | 11 | as7262.set_gain(64) 12 | 13 | as7262.set_integration_time(17.857) 14 | 15 | as7262.set_measurement_mode(2) 16 | 17 | # as7262.set_illumination_led_current(12.5) 18 | as7262.set_illumination_led(1) 19 | # as7262.set_indicator_led_current(2) 20 | # as7262.set_indicator_led(1) 21 | 22 | try: 23 | while True: 24 | values = as7262.get_calibrated_values() 25 | print(""" 26 | Red: {} 27 | Orange: {} 28 | Yellow: {} 29 | Green: {} 30 | Blue: {} 31 | Violet: {}""".format(*values)) 32 | except KeyboardInterrupt: 33 | as7262.set_measurement_mode(3) 34 | as7262.set_illumination_led(0) 35 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script handles some basic QA checks on the source 4 | 5 | NOPOST=$1 6 | LIBRARY_NAME=$(hatch project metadata name) 7 | LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') 8 | POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') 9 | TERM=${TERM:="xterm-256color"} 10 | 11 | success() { 12 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 13 | } 14 | 15 | inform() { 16 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 17 | } 18 | 19 | warning() { 20 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 21 | } 22 | 23 | while [[ $# -gt 0 ]]; do 24 | K="$1" 25 | case $K in 26 | -p|--nopost) 27 | NOPOST=true 28 | shift 29 | ;; 30 | *) 31 | if [[ $1 == -* ]]; then 32 | printf "Unrecognised option: %s\n" "$1"; 33 | exit 1 34 | fi 35 | POSITIONAL_ARGS+=("$1") 36 | shift 37 | esac 38 | done 39 | 40 | inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" 41 | 42 | inform "Checking for trailing whitespace..." 43 | if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then 44 | warning "Trailing whitespace found!" 45 | exit 1 46 | else 47 | success "No trailing whitespace found." 48 | fi 49 | printf "\n" 50 | 51 | inform "Checking for DOS line-endings..." 52 | if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then 53 | warning "DOS line-endings found!" 54 | exit 1 55 | else 56 | success "No DOS line-endings found." 57 | fi 58 | printf "\n" 59 | 60 | inform "Checking CHANGELOG.md..." 61 | if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then 62 | warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." 63 | exit 1 64 | else 65 | success "Changes found for version ${LIBRARY_VERSION}." 66 | fi 67 | printf "\n" 68 | 69 | inform "Checking for git tag ${LIBRARY_VERSION}..." 70 | if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then 71 | warning "Missing git tag for version ${LIBRARY_VERSION}" 72 | fi 73 | printf "\n" 74 | 75 | if [[ $NOPOST ]]; then 76 | inform "Checking for .postN on library version..." 77 | if [[ "$POST_VERSION" != "" ]]; then 78 | warning "Found .$POST_VERSION on library version." 79 | inform "Please only use these for testpypi releases." 80 | exit 1 81 | else 82 | success "OK" 83 | fi 84 | fi 85 | -------------------------------------------------------------------------------- /examples/bargraph.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | from as7262 import AS7262 6 | 7 | as7262 = AS7262() 8 | 9 | BAR_CHAR = u'\u2588' 10 | 11 | ANSI_COLOR_RED = '\x1b[31m' 12 | ANSI_COLOR_GREEN = '\x1b[32m' 13 | ANSI_COLOR_YELLOW = '\x1b[33m' 14 | ANSI_COLOR_BLUE = '\x1b[34m' 15 | ANSI_COLOR_MAGENTA = '\x1b[35m' 16 | 17 | MAX_VALUE = 14000.0 18 | BAR_WIDTH = 25 19 | 20 | as7262.set_gain(64) 21 | as7262.set_integration_time(17.857) 22 | as7262.set_measurement_mode(2) 23 | as7262.set_illumination_led(1) 24 | 25 | try: 26 | input = raw_input 27 | except NameError: 28 | pass 29 | 30 | input("Setting white point baseline.\n\nHold a white sheet of paper ~5cm in front of the sensor and press a key...\n") 31 | baseline = as7262.get_calibrated_values() 32 | time.sleep(1) 33 | input("Baseline set. Press a key to continue...\n") 34 | sys.stdout.flush() 35 | 36 | try: 37 | while True: 38 | tcols, _ = os.get_terminal_size() 39 | values = as7262.get_calibrated_values() 40 | values = [int(x/y*MAX_VALUE) for x,y in zip(list(values), list(baseline))] 41 | values = [int(min(value, MAX_VALUE) / MAX_VALUE * BAR_WIDTH) for value in values] 42 | red, orange, yellow, green, blue, violet = [(BAR_CHAR * value) + (' ' * (BAR_WIDTH - value)) for value in values] 43 | 44 | sys.stdout.write('\x1b[0;1H') 45 | bargraph =u""" Spectrometer Bar Graph 46 | --------------------------------- 47 | |Red: {}{}\x1b[0m| 48 | |Orange: {}{}\x1b[0m| 49 | |Yellow: {}{}\x1b[0m| 50 | |Green: {}{}\x1b[0m| 51 | |Blue: {}{}\x1b[0m| 52 | |Violet: {}{}\x1b[0m| 53 | --------------------------------- 54 | 55 | """.format( 56 | ANSI_COLOR_RED, red, 57 | ANSI_COLOR_YELLOW, orange, 58 | ANSI_COLOR_YELLOW, yellow, 59 | ANSI_COLOR_GREEN, green, 60 | ANSI_COLOR_BLUE, blue, 61 | ANSI_COLOR_MAGENTA, violet 62 | ) 63 | 64 | bargraph = "\n".join(line.ljust(tcols, " ") for line in bargraph.split("\n")) 65 | sys.stdout.write(bargraph) 66 | sys.stdout.flush() 67 | time.sleep(0.5) 68 | 69 | except KeyboardInterrupt: 70 | as7262.set_measurement_mode(3) 71 | as7262.set_illumination_led(0) 72 | 73 | -------------------------------------------------------------------------------- /examples/spectrum.py: -------------------------------------------------------------------------------- 1 | from as7262 import AS7262 2 | 3 | as7262 = AS7262() 4 | 5 | as7262.set_gain(64) 6 | as7262.set_integration_time(17.857) 7 | as7262.set_measurement_mode(2) 8 | as7262.set_illumination_led(1) 9 | 10 | try: 11 | while True: 12 | values = as7262.get_calibrated_values() 13 | print(""" 14 | Red: {} 15 | Orange: {} 16 | Yellow: {} 17 | Green: {} 18 | Blue: {} 19 | Violet: {}""".format(*values)) 20 | 21 | except KeyboardInterrupt: 22 | as7262.set_measurement_mode(3) 23 | as7262.set_illumination_led(0) 24 | 25 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') 3 | CONFIG_FILE=config.txt 4 | CONFIG_DIR="/boot/firmware" 5 | DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") 6 | CONFIG_BACKUP=false 7 | APT_HAS_UPDATED=false 8 | RESOURCES_TOP_DIR="$HOME/Pimoroni" 9 | VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" 10 | VENV_DIR="$HOME/.virtualenvs/pimoroni" 11 | USAGE="./install.sh (--unstable)" 12 | POSITIONAL_ARGS=() 13 | FORCE=false 14 | UNSTABLE=false 15 | PYTHON="python" 16 | CMD_ERRORS=false 17 | 18 | 19 | user_check() { 20 | if [ "$(id -u)" -eq 0 ]; then 21 | fatal "Script should not be run as root. Try './install.sh'\n" 22 | fi 23 | } 24 | 25 | confirm() { 26 | if $FORCE; then 27 | true 28 | else 29 | read -r -p "$1 [y/N] " response < /dev/tty 30 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 31 | true 32 | else 33 | false 34 | fi 35 | fi 36 | } 37 | 38 | success() { 39 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 40 | } 41 | 42 | inform() { 43 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 44 | } 45 | 46 | warning() { 47 | echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" 48 | } 49 | 50 | fatal() { 51 | echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" 52 | exit 1 53 | } 54 | 55 | find_config() { 56 | if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then 57 | CONFIG_DIR="/boot" 58 | if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then 59 | fatal "Could not find $CONFIG_FILE!" 60 | fi 61 | fi 62 | inform "Using $CONFIG_FILE in $CONFIG_DIR" 63 | } 64 | 65 | venv_bash_snippet() { 66 | inform "Checking for $VENV_BASH_SNIPPET\n" 67 | if [ ! -f "$VENV_BASH_SNIPPET" ]; then 68 | inform "Creating $VENV_BASH_SNIPPET\n" 69 | mkdir -p "$RESOURCES_TOP_DIR" 70 | cat << EOF > "$VENV_BASH_SNIPPET" 71 | # Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate 72 | # the Pimoroni virtual environment automagically! 73 | VENV_DIR="$VENV_DIR" 74 | if [ ! -f \$VENV_DIR/bin/activate ]; then 75 | printf "Creating user Python environment in \$VENV_DIR, please wait...\n" 76 | mkdir -p \$VENV_DIR 77 | python3 -m venv --system-site-packages \$VENV_DIR 78 | fi 79 | printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" 80 | source \$VENV_DIR/bin/activate 81 | EOF 82 | fi 83 | } 84 | 85 | venv_check() { 86 | PYTHON_BIN=$(which "$PYTHON") 87 | if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then 88 | printf "This script should be run in a virtual Python environment.\n" 89 | if confirm "Would you like us to create and/or use a default one?"; then 90 | printf "\n" 91 | if [ ! -f "$VENV_DIR/bin/activate" ]; then 92 | inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" 93 | mkdir -p "$VENV_DIR" 94 | /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages 95 | venv_bash_snippet 96 | # shellcheck disable=SC1091 97 | source "$VENV_DIR/bin/activate" 98 | else 99 | inform "Activating existing virtual Python environment in $VENV_DIR\n" 100 | printf "source \"%s/bin/activate\"\n" "$VENV_DIR" 101 | # shellcheck disable=SC1091 102 | source "$VENV_DIR/bin/activate" 103 | fi 104 | else 105 | printf "\n" 106 | fatal "Please create and/or activate a virtual Python environment and try again!\n" 107 | fi 108 | fi 109 | printf "\n" 110 | } 111 | 112 | check_for_error() { 113 | if [ $? -ne 0 ]; then 114 | CMD_ERRORS=true 115 | warning "^^^ 😬 previous command did not exit cleanly!" 116 | fi 117 | } 118 | 119 | function do_config_backup { 120 | if [ ! $CONFIG_BACKUP == true ]; then 121 | CONFIG_BACKUP=true 122 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 123 | inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" 124 | sudo cp "$CONFIG_DIR/$CONFIG_FILE" "$CONFIG_DIR/$FILENAME" 125 | mkdir -p "$RESOURCES_TOP_DIR/config-backups/" 126 | cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" 127 | if [ -f "$UNINSTALLER" ]; then 128 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" 129 | fi 130 | fi 131 | } 132 | 133 | function apt_pkg_install { 134 | PACKAGES_NEEDED=() 135 | PACKAGES_IN=("$@") 136 | # Check the list of packages and only run update/install if we need to 137 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 138 | PACKAGE="${PACKAGES_IN[$i]}" 139 | if [ "$PACKAGE" == "" ]; then continue; fi 140 | printf "Checking for %s\n" "$PACKAGE" 141 | dpkg -L "$PACKAGE" > /dev/null 2>&1 142 | if [ "$?" == "1" ]; then 143 | PACKAGES_NEEDED+=("$PACKAGE") 144 | fi 145 | done 146 | PACKAGES="${PACKAGES_NEEDED[*]}" 147 | if ! [ "$PACKAGES" == "" ]; then 148 | printf "\n" 149 | inform "Installing missing packages: $PACKAGES" 150 | if [ ! $APT_HAS_UPDATED ]; then 151 | sudo apt update 152 | APT_HAS_UPDATED=true 153 | fi 154 | # shellcheck disable=SC2086 155 | sudo apt install -y $PACKAGES 156 | check_for_error 157 | if [ -f "$UNINSTALLER" ]; then 158 | echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" 159 | fi 160 | fi 161 | } 162 | 163 | function pip_pkg_install { 164 | # A null Keyring prevents pip stalling in the background 165 | PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" 166 | check_for_error 167 | } 168 | 169 | while [[ $# -gt 0 ]]; do 170 | K="$1" 171 | case $K in 172 | -u|--unstable) 173 | UNSTABLE=true 174 | shift 175 | ;; 176 | -f|--force) 177 | FORCE=true 178 | shift 179 | ;; 180 | -p|--python) 181 | PYTHON=$2 182 | shift 183 | shift 184 | ;; 185 | *) 186 | if [[ $1 == -* ]]; then 187 | printf "Unrecognised option: %s\n" "$1"; 188 | printf "Usage: %s\n" "$USAGE"; 189 | exit 1 190 | fi 191 | POSITIONAL_ARGS+=("$1") 192 | shift 193 | esac 194 | done 195 | 196 | printf "Installing %s...\n\n" "$LIBRARY_NAME" 197 | 198 | user_check 199 | venv_check 200 | 201 | if [ ! -f "$(which "$PYTHON")" ]; then 202 | fatal "Python path %s not found!\n" "$PYTHON" 203 | fi 204 | 205 | PYTHON_VER=$($PYTHON --version) 206 | 207 | inform "Checking Dependencies. Please wait..." 208 | 209 | # Install toml and try to read pyproject.toml into bash variables 210 | 211 | pip_pkg_install toml 212 | 213 | CONFIG_VARS=$( 214 | $PYTHON - < "$UNINSTALLER" 257 | printf "It's recommended you run these steps manually.\n" 258 | printf "If you want to run the full script, open it in\n" 259 | printf "an editor and remove 'exit 1' from below.\n" 260 | exit 1 261 | source $VIRTUAL_ENV/bin/activate 262 | EOF 263 | 264 | printf "\n" 265 | 266 | inform "Installing for $PYTHON_VER...\n" 267 | 268 | # Install apt packages from pyproject.toml / tool.pimoroni.apt_packages 269 | apt_pkg_install "${APT_PACKAGES[@]}" 270 | 271 | printf "\n" 272 | 273 | if $UNSTABLE; then 274 | warning "Installing unstable library from source.\n" 275 | pip_pkg_install . 276 | else 277 | inform "Installing stable library from pypi.\n" 278 | pip_pkg_install "$LIBRARY_NAME" 279 | fi 280 | 281 | # shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag 282 | if [ $? -eq 0 ]; then 283 | success "Done!\n" 284 | echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" 285 | fi 286 | 287 | find_config 288 | 289 | printf "\n" 290 | 291 | # Run the setup commands from pyproject.toml / tool.pimoroni.commands 292 | 293 | inform "Running setup commands...\n" 294 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 295 | CMD="${SETUP_CMDS[$i]}" 296 | # Attempt to catch anything that touches config.txt and trigger a backup 297 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then 298 | do_config_backup 299 | fi 300 | if [[ ! "$CMD" == printf* ]]; then 301 | printf "Running: \"%s\"\n" "$CMD" 302 | fi 303 | eval "$CMD" 304 | check_for_error 305 | done 306 | 307 | printf "\n" 308 | 309 | # Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt 310 | 311 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 312 | CONFIG_LINE="${CONFIG_TXT[$i]}" 313 | if ! [ "$CONFIG_LINE" == "" ]; then 314 | do_config_backup 315 | inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" 316 | sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE 317 | if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then 318 | printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE 319 | fi 320 | fi 321 | done 322 | 323 | printf "\n" 324 | 325 | # Just a straight copy of the examples/ dir into ~/Pimoroni/board/examples 326 | 327 | if [ -d "examples" ]; then 328 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 329 | inform "Copying examples to $RESOURCES_DIR" 330 | cp -r examples/ "$RESOURCES_DIR" 331 | echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" 332 | success "Done!" 333 | fi 334 | fi 335 | 336 | printf "\n" 337 | 338 | # Use pdoc to generate basic documentation from the installed module 339 | 340 | if confirm "Would you like to generate documentation?"; then 341 | inform "Installing pdoc. Please wait..." 342 | pip_pkg_install pdoc 343 | inform "Generating documentation.\n" 344 | if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then 345 | inform "Documentation saved to $RESOURCES_DIR/docs" 346 | success "Done!" 347 | else 348 | warning "Error: Failed to generate documentation." 349 | fi 350 | fi 351 | 352 | printf "\n" 353 | 354 | if [ "$CMD_ERRORS" = true ]; then 355 | warning "One or more setup commands appear to have failed." 356 | printf "This might prevent things from working properly.\n" 357 | printf "Make sure your OS is up to date and try re-running this installer.\n" 358 | printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" 359 | else 360 | success "\nAll done!" 361 | fi 362 | 363 | printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" 364 | printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" 365 | 366 | if [ "$CMD_ERRORS" = true ]; then 367 | exit 1 368 | else 369 | exit 0 370 | fi 371 | -------------------------------------------------------------------------------- /library/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = as7262 3 | omit = 4 | .tox/* 5 | as7262/__main__.py 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-fancy-pypi-readme"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "as7262" 7 | dynamic = ["version", "readme"] 8 | description = "Library for the AS7262 spectral sensor" 9 | license = {file = "LICENSE"} 10 | requires-python = ">= 3.7" 11 | authors = [ 12 | { name = "Philip Howard", email = "phil@pimoroni.com" }, 13 | ] 14 | maintainers = [ 15 | { name = "Philip Howard", email = "phil@pimoroni.com" }, 16 | ] 17 | keywords = [ 18 | "Pi", 19 | "Raspberry", 20 | ] 21 | classifiers = [ 22 | "Development Status :: 4 - Beta", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: POSIX :: Linux", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3 :: Only", 33 | "Topic :: Software Development", 34 | "Topic :: Software Development :: Libraries", 35 | "Topic :: System :: Hardware", 36 | ] 37 | dependencies = [ 38 | "i2cdevice>=1.0.0" 39 | ] 40 | 41 | [project.urls] 42 | GitHub = "https://www.github.com/pimoroni/as7262-python" 43 | Homepage = "https://www.pimoroni.com" 44 | 45 | [tool.hatch.version] 46 | path = "as7262/__init__.py" 47 | 48 | [tool.hatch.build] 49 | include = [ 50 | "as7262", 51 | "README.md", 52 | "CHANGELOG.md", 53 | "LICENSE" 54 | ] 55 | 56 | [tool.hatch.build.targets.sdist] 57 | include = [ 58 | "*" 59 | ] 60 | exclude = [ 61 | ".*", 62 | "dist" 63 | ] 64 | 65 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 66 | content-type = "text/markdown" 67 | fragments = [ 68 | { path = "README.md" }, 69 | { text = "\n" }, 70 | { path = "CHANGELOG.md" } 71 | ] 72 | 73 | [tool.ruff] 74 | exclude = [ 75 | '.tox', 76 | '.egg', 77 | '.git', 78 | '__pycache__', 79 | 'build', 80 | 'dist' 81 | ] 82 | line-length = 200 83 | 84 | [tool.codespell] 85 | skip = """ 86 | ./.tox,\ 87 | ./.egg,\ 88 | ./.git,\ 89 | ./__pycache__,\ 90 | ./build,\ 91 | ./dist.\ 92 | """ 93 | 94 | [tool.isort] 95 | line_length = 200 96 | 97 | [tool.check-manifest] 98 | ignore = [ 99 | '.stickler.yml', 100 | 'boilerplate.md', 101 | 'check.sh', 102 | 'install.sh', 103 | 'uninstall.sh', 104 | 'Makefile', 105 | 'tox.ini', 106 | 'tests/*', 107 | 'examples/*', 108 | '.coveragerc', 109 | 'requirements-dev.txt' 110 | ] 111 | 112 | [tool.pimoroni] 113 | apt_packages = [] 114 | configtxt = [] 115 | commands = [ 116 | "sudo raspi-config nonint do_i2c 0" 117 | ] 118 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | check-manifest 2 | ruff 3 | codespell 4 | isort 5 | twine 6 | hatch 7 | hatch-fancy-pypi-readme 8 | tox 9 | pdoc 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test Fixtures. 2 | 3 | This __init__.py is required for relative import of test common tools and test discovery in VSCode 4 | 5 | """ 6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | 4 | import mock 5 | import pytest 6 | 7 | from .tools import SMBusFakeAS7262 8 | 9 | 10 | @pytest.fixture 11 | def smbus(): 12 | smbus = mock.Mock() 13 | smbus.SMBus = SMBusFakeAS7262 14 | sys.modules['smbus2'] = smbus 15 | yield smbus 16 | 17 | 18 | @pytest.fixture 19 | def AS7262(): 20 | from as7262 import AS7262 21 | yield AS7262 22 | del sys.modules['as7262'] -------------------------------------------------------------------------------- /tests/test_features.py: -------------------------------------------------------------------------------- 1 | # noqa D100 2 | from .tools import CALIBRATED_VALUES 3 | 4 | 5 | def test_set_integration_time(smbus, AS7262): 6 | """Test the set_integration_time method against various values.""" 7 | as7262 = AS7262() 8 | 9 | # Integration time is stored as 2.8ms per lsb 10 | # so returned values experience quantization 11 | # int(50/2.8)*2.8 == 50.0 12 | as7262.set_integration_time(50) 13 | assert as7262._as7262.INTEGRATION_TIME.get_ms() == 50.0 14 | 15 | # For example: 90 will alias to 89.6 16 | # int(90/2.8)*2.8 == 89.6 17 | as7262.set_integration_time(90) 18 | assert round(as7262._as7262.INTEGRATION_TIME.get_ms(), 1) == 89.6 19 | 20 | # All input values are masked by i2cdevice according 21 | # to the mask supplied. 22 | # In the case of Integration Time this is 0xFF 23 | # A value of 99999 multiplied by 2.8 and masked would 24 | # result in 189 being written to the device. 25 | as7262.set_integration_time(99999) 26 | assert as7262._as7262.INTEGRATION_TIME.get_ms() == (int(99999 * 2.8) & 0xFF) / 2.8 27 | 28 | 29 | def test_set_gain(smbus, AS7262): 30 | """Test the set_gain method against various values.""" 31 | as7262 = AS7262() 32 | 33 | as7262.set_gain(1) 34 | assert as7262._as7262.CONTROL.get_gain_x() == 1 35 | 36 | # Should snap to the highest gain value 37 | as7262.set_gain(999) 38 | assert as7262._as7262.CONTROL.get_gain_x() == 64 39 | 40 | # Should snap to the lowest gain value 41 | as7262.set_gain(-1) 42 | assert as7262._as7262.CONTROL.get_gain_x() == 1 43 | 44 | 45 | def test_set_measurement_mode(smbus, AS7262): 46 | """Test the set_measurement_mode method.""" 47 | as7262 = AS7262() 48 | 49 | as7262.set_measurement_mode(2) 50 | assert as7262._as7262.CONTROL.get_measurement_mode() == 2 51 | 52 | 53 | def test_set_illumination_led_current(smbus, AS7262): 54 | """Test the set_illumination_led_current method.""" 55 | as7262 = AS7262() 56 | 57 | as7262.set_illumination_led_current(12.5) 58 | assert as7262._as7262.LED_CONTROL.get_illumination_current_limit_ma() == 12.5 59 | 60 | as7262.set_illumination_led_current(20) 61 | assert as7262._as7262.LED_CONTROL.get_illumination_current_limit_ma() == 25 62 | 63 | as7262.set_illumination_led_current(101) 64 | assert as7262._as7262.LED_CONTROL.get_illumination_current_limit_ma() == 100 65 | 66 | 67 | def test_set_indicator_led_current(smbus, AS7262): 68 | """Test the set_indicator_led_current method.""" 69 | as7262 = AS7262() 70 | 71 | as7262.set_indicator_led_current(4) 72 | assert as7262._as7262.LED_CONTROL.get_indicator_current_limit_ma() == 4 73 | 74 | as7262.set_indicator_led_current(9) 75 | assert as7262._as7262.LED_CONTROL.get_indicator_current_limit_ma() == 8 76 | 77 | as7262.set_indicator_led_current(0) 78 | assert as7262._as7262.LED_CONTROL.get_indicator_current_limit_ma() == 1 79 | 80 | 81 | def test_indicator_led(smbus, AS7262): 82 | """Test the indicator_led method.""" 83 | as7262 = AS7262() 84 | 85 | as7262.set_indicator_led(1) 86 | assert as7262._as7262.LED_CONTROL.get_indicator_enable() == 1 87 | 88 | 89 | def test_illumination_led(smbus, AS7262): 90 | """Test the illumination_led method.""" 91 | as7262 = AS7262() 92 | 93 | as7262.set_illumination_led(1) 94 | assert as7262._as7262.LED_CONTROL.get_illumination_enable() == 1 95 | 96 | 97 | def test_soft_reset(smbus, AS7262): 98 | """Test the soft_reset method.""" 99 | as7262 = AS7262() 100 | 101 | as7262.soft_reset() 102 | assert as7262._as7262.CONTROL.get_reset() == 1 103 | 104 | 105 | def test_get_calibrated_values(smbus, AS7262): 106 | """Test against fake calibrated values stored in hardware mock.""" 107 | as7262 = AS7262() 108 | 109 | values = as7262.get_calibrated_values() 110 | 111 | # Deal with floating point nonsense 112 | values = [round(x, 1) for x in values] 113 | 114 | assert values == CALIBRATED_VALUES 115 | -------------------------------------------------------------------------------- /tests/test_setup.py: -------------------------------------------------------------------------------- 1 | # noqa D100 2 | 3 | 4 | def test_fw_info(smbus, AS7262): 5 | """Test against fake device information stored in hardware mock.""" 6 | as7262 = AS7262() 7 | 8 | hw_type, hw_version, fw_version = as7262.get_version() 9 | 10 | assert hw_version == 0x77 11 | assert hw_type == 0x88 12 | assert fw_version == '15.63.62' 13 | -------------------------------------------------------------------------------- /tests/tools.py: -------------------------------------------------------------------------------- 1 | """Test tools for the AS7262 sensor.""" 2 | import struct 3 | 4 | from i2cdevice import MockSMBus 5 | 6 | CALIBRATED_VALUES = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6] 7 | 8 | REG_STATUS = 0x00 9 | REG_WRITE = 0x01 10 | REG_READ = 0x02 11 | 12 | 13 | class SMBusFakeAS7262(MockSMBus): 14 | """Fake the AS7262 non-standard i2c-based protocol. 15 | 16 | The AS7262 uses 3 registers- status, write and read. 17 | 18 | Internally it maintains a register pointer that is set 19 | using a write operation (register 0x01). 20 | 21 | This pointer uses the 7th bit (0b10000000) to indicate 22 | a write versus a read but we can mostly ignore it. 23 | 24 | Any write or read operation will begin with a write 25 | to register 0x01 - the real "write" register. 26 | 27 | A read will then follow with a read from register 0x02 28 | - the real "read" register. 29 | 30 | In our case, we use the previously written register 31 | as a pointer into self.regs, which is our virtual 32 | register space. 33 | 34 | A read to 0x00 will always return the status, 35 | regardless of how the pointer is currently set. 36 | 37 | This mimics the AS7262's behaviour closely enough 38 | to facilitate testing. 39 | 40 | """ 41 | 42 | def __init__(self, i2c_bus): 43 | """Initialise the class. 44 | 45 | :param i2c_bus: i2c bus ID. 46 | 47 | """ 48 | MockSMBus.__init__(self, i2c_bus) 49 | self.status = 0b01 # Fake status register 50 | self.ptr = None # Fake register pointer 51 | 52 | # Virtual registers, these contain the data actually used 53 | self.regs[0x00] = 0x88 # Fake HW type 54 | self.regs[0x01] = 0x77 # Fake HW version 55 | self.regs[0x02] = 0xFE # Fake FW version MSB (Sub, Minor) 56 | self.regs[0x03] = 0xFF # Fake FW version LSB (Minor, Major) 57 | self.regs[0x04] = 0x02 # Control Register 58 | 59 | # Prime the Calibrated Data registers with fake data 60 | self.regs[0x14:24] = [ord(c) if isinstance(c, str) else c for c in struct.pack( 61 | '>ffffff', 62 | *reversed(CALIBRATED_VALUES) 63 | )] 64 | 65 | # Major = 0b1111 = 15 66 | # Minor = 0b111111 = 63 67 | # Sub = 0b111110 = 62 68 | 69 | def write_byte_data(self, i2c_address, register, value): 70 | """Write a single byte.""" 71 | if self.ptr is None and register == REG_WRITE: 72 | self.ptr = value & 0b1111111 # Mask out write bit 73 | 74 | elif self.ptr is not None: 75 | self.regs[self.ptr] = value 76 | self.ptr = None 77 | 78 | def read_byte_data(self, i2c_address, register): 79 | """Read a single byte.""" 80 | if register == REG_STATUS: 81 | return self.status 82 | 83 | elif self.ptr is not None and register == REG_READ: 84 | value = self.regs[self.ptr] 85 | self.ptr = None 86 | return value 87 | 88 | return 0 89 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py,qa 3 | skip_missing_interpreters = True 4 | isolated_build = true 5 | minversion = 4.0.0 6 | 7 | [testenv] 8 | commands = 9 | coverage run -m pytest -v -r wsx 10 | coverage report 11 | deps = 12 | mock 13 | pytest>=3.1 14 | pytest-cov 15 | build 16 | 17 | [testenv:qa] 18 | commands = 19 | check-manifest 20 | python -m build --no-isolation 21 | python -m twine check dist/* 22 | isort --check . 23 | ruff check . 24 | codespell . 25 | deps = 26 | check-manifest 27 | ruff 28 | codespell 29 | isort 30 | twine 31 | build 32 | hatch 33 | hatch-fancy-pypi-readme 34 | 35 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FORCE=false 4 | LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') 5 | RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME 6 | PYTHON="python" 7 | 8 | 9 | venv_check() { 10 | PYTHON_BIN=$(which $PYTHON) 11 | if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then 12 | printf "This script should be run in a virtual Python environment.\n" 13 | exit 1 14 | fi 15 | } 16 | 17 | user_check() { 18 | if [ "$(id -u)" -eq 0 ]; then 19 | printf "Script should not be run as root. Try './uninstall.sh'\n" 20 | exit 1 21 | fi 22 | } 23 | 24 | confirm() { 25 | if $FORCE; then 26 | true 27 | else 28 | read -r -p "$1 [y/N] " response < /dev/tty 29 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 30 | true 31 | else 32 | false 33 | fi 34 | fi 35 | } 36 | 37 | prompt() { 38 | read -r -p "$1 [y/N] " response < /dev/tty 39 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 40 | true 41 | else 42 | false 43 | fi 44 | } 45 | 46 | success() { 47 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 48 | } 49 | 50 | inform() { 51 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 52 | } 53 | 54 | warning() { 55 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 56 | } 57 | 58 | printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" 59 | 60 | user_check 61 | venv_check 62 | 63 | printf "Uninstalling for Python 3...\n" 64 | $PYTHON -m pip uninstall "$LIBRARY_NAME" 65 | 66 | if [ -d "$RESOURCES_DIR" ]; then 67 | if confirm "Would you like to delete $RESOURCES_DIR?"; then 68 | rm -r "$RESOURCES_DIR" 69 | fi 70 | fi 71 | 72 | printf "Done!\n" 73 | --------------------------------------------------------------------------------