├── .github └── workflows │ ├── build.yml │ ├── qa.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── check.sh ├── examples └── ltr559.py ├── i2cdevice ├── __init__.py └── adapter.py ├── install.sh ├── pyproject.toml ├── requirements-dev.txt ├── tests ├── test_adapters.py ├── test_defaultbus.py ├── test_device.py ├── test_mocksmbus.py ├── test_registerproxy.py ├── test_set_and_get.py └── test_utils.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@v3 23 | 24 | - name: Set up Python ${{ matrix.python }} 25 | uses: actions/setup-python@v3 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@v3 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 | 14 | env: 15 | TERM: xterm-256color 16 | 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up Python '3,11' 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: '3.11' 25 | 26 | - name: Install Dependencies 27 | run: | 28 | make dev-deps 29 | 30 | - name: Run Quality Assurance 31 | run: | 32 | make qa 33 | 34 | - name: Run Code Checks 35 | run: | 36 | make check 37 | -------------------------------------------------------------------------------- /.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@v3 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 | .coverage 18 | .pytest_cache 19 | .tox 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | ----- 3 | 4 | * Enhancement: Repackage to pyproject.toml/hatchling 5 | * Switch i2c_dev fallback from smbus to smbus2 6 | 7 | 0.0.7 8 | ----- 9 | 10 | * BugFix: Prevent .get from reading multiple times (thanks @dkao) 11 | * Enhancement: Better error output (thanks Kamil Klimek) 12 | 13 | 0.0.6 14 | ----- 15 | 16 | * New API methods set and get 17 | 18 | 0.0.5 19 | ----- 20 | 21 | * Bump to stable release 22 | 23 | 0.0.4 24 | ----- 25 | 26 | * Bugfixes 27 | 28 | 0.0.3 29 | ----- 30 | 31 | * Added License 32 | 33 | 0.0.2 34 | ----- 35 | 36 | * Major Refactor 37 | 38 | 0.0.1 39 | ----- 40 | 41 | * Initial Release 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.txt 2 | include LICENSE.txt 3 | include README.rst 4 | include setup.py 5 | recursive-include i2cdevice *.py 6 | -------------------------------------------------------------------------------- /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 | install: 26 | ./install.sh --unstable 27 | 28 | uninstall: 29 | ./uninstall.sh 30 | 31 | dev-deps: 32 | python3 -m pip install -r requirements-dev.txt 33 | sudo apt install dos2unix 34 | 35 | check: 36 | @bash check.sh 37 | 38 | qa: 39 | tox -e qa 40 | 41 | pytest: 42 | tox -e py 43 | 44 | nopost: 45 | @bash check.sh --nopost 46 | 47 | tag: 48 | git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" 49 | 50 | build: check 51 | @hatch build 52 | 53 | clean: 54 | -rm -r dist 55 | 56 | testdeploy: build 57 | twine upload --repository testpypi dist/* 58 | 59 | deploy: nopost build 60 | twine upload dist/* 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i2cdevice 2 | 3 | [![Build Status](https://travis-ci.com/pimoroni/i2cdevice-python.svg?branch=master)](https://travis-ci.com/pimoroni/i2cdevice-python) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/i2cdevice-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/i2cdevice-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/i2cdevice.svg)](https://pypi.python.org/pypi/i2cdevice) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/i2cdevice.svg)](https://pypi.python.org/pypi/i2cdevice) 7 | 8 | i2cdevice is a Python framework aimed at dealing with common SMBus/i2c device interaction patterns. 9 | 10 | This project aims to make group-up implementations of Python libraries for i2c devices easier, simpler and inherently self-documenting. 11 | 12 | It does this by separating a detailed description of the hardware registers and how they should be manipulated into a structured definition language. 13 | 14 | This project does not aim to help you make a public API for Python devices- that should be built on top of the fundamentals presented here. 15 | 16 | # Using This Library 17 | 18 | You should generally aim for a 1:1 representation of the hardware registers in the device you're implementing, even if you don't plan to use all the functionality. Having the full register set implemented allows for the easy addition of new features in future. 19 | 20 | Check out the libraries listed below for real-world examples. 21 | 22 | # Features 23 | 24 | * Classes for describing devices, registers and individual bit fields within registers in a fashion which maps closely with the datasheet 25 | * Value translation from real world numbers (such as `512ms`) to register values (such as `0b111`) and back again 26 | * Read registers into a namedtuple of fields using `get` 27 | * Write multiple register fields in a transaction using `set` with keyword arguments 28 | * Support for treating multiple-bytes as a single value, or single register with multiple values 29 | 30 | # Built With i2cdevice 31 | 32 | * bme280 - https://github.com/pimoroni/bme280-python 33 | * bmp280 - https://github.com/pimoroni/bmp280-python 34 | * bh1745 - https://github.com/pimoroni/bh1745-python 35 | * as7262 - https://github.com/pimoroni/as7262-python 36 | * lsm303d - https://github.com/pimoroni/lsm303d-python 37 | * ltr559 - https://github.com/pimoroni/ltr559-python 38 | * ads1015 - https://github.com/pimoroni/ads1015-python 39 | 40 | # Examples 41 | 42 | The below example defines the `ALS_CONTROL` register on an ltr559, with register address `0x80`. 43 | 44 | It has 3 fields; gain - which is mapped to real world values - and sw_reset/mode which are single bit flags. 45 | 46 | ```python 47 | ALS_CONTROL = Register('ALS_CONTROL', 0x80, fields=( 48 | BitField('gain', 0b00011100, values_map={1: 0b000, 2: 0b001, 4: 0b011, 8:0b011, 48:0b110, 96:0b111}), 49 | BitField('sw_reset', 0b00000010), 50 | BitField('mode', 0b00000001) 51 | )) 52 | ``` 53 | 54 | A lookup table is not required for values, however, a function can be used to translate values from and to a format that the device understands. 55 | 56 | The below example uses `i2cdevice._byte_swap` to change the endianness of two 16bit values before they are stored/retrieved. 57 | 58 | ```python 59 | # This will address 0x88, 0x89, 0x8A and 0x8B as a continuous 32bit register 60 | ALS_DATA = Register('ALS_DATA', 0x88, fields=( 61 | BitField('ch1', 0xFFFF0000, bitwidth=16, values_in=_byte_swap, values_out=_byte_swap), 62 | BitField('ch0', 0x0000FFFF, bitwidth=16, values_in=_byte_swap, values_out=_byte_swap) 63 | ), read_only=True, bitwidth=32) 64 | ``` 65 | 66 | A "Register" and its "BitField"s define a set of rules and logic for detailing with the hardware register which is interpreted by the device class. Registers are declared on a device using the `registers=()` keyword argument: 67 | 68 | ```python 69 | I2C_ADDR = 0x23 70 | ltr559 = Device(I2C_ADDR, bit_width=8, registers=( 71 | ALS_CONTROL, 72 | ALS_DATA 73 | )) 74 | ``` 75 | 76 | ## Reading Registers 77 | 78 | One configured a register's fields can be read into a namedtuple using the `get` method: 79 | 80 | ```python 81 | register_values = ltr559.get('ALS_CONTROL') 82 | gain = register_values.gain 83 | sw_reset = register_values.sw_reset 84 | mode = register_values.mode 85 | ``` 86 | 87 | ## Writing Registers 88 | 89 | The namedtuple returned from `get` is immutable and does not attempt to map values back to the hardware, in order to write one or more fields to a register you must use `set` with a keyword argument for each field: 90 | 91 | ```python 92 | ltr559.set('ALS_CONTROL', 93 | gain=4, 94 | sw_reset=1) 95 | ``` 96 | 97 | This will read the register state from the device, update the bitfields accordingly and write the result back. 98 | -------------------------------------------------------------------------------- /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 | 10 | success() { 11 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 12 | } 13 | 14 | inform() { 15 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 16 | } 17 | 18 | warning() { 19 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 20 | } 21 | 22 | while [[ $# -gt 0 ]]; do 23 | K="$1" 24 | case $K in 25 | -p|--nopost) 26 | NOPOST=true 27 | shift 28 | ;; 29 | *) 30 | if [[ $1 == -* ]]; then 31 | printf "Unrecognised option: $1\n"; 32 | exit 1 33 | fi 34 | POSITIONAL_ARGS+=("$1") 35 | shift 36 | esac 37 | done 38 | 39 | inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" 40 | 41 | inform "Checking for trailing whitespace..." 42 | grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO 43 | if [[ $? -eq 0 ]]; 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 | grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile 53 | if [[ $? -eq 0 ]]; then 54 | warning "DOS line-endings found!" 55 | exit 1 56 | else 57 | success "No DOS line-endings found." 58 | fi 59 | printf "\n" 60 | 61 | inform "Checking CHANGELOG.md..." 62 | cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 63 | if [[ $? -eq 1 ]]; then 64 | warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." 65 | exit 1 66 | else 67 | success "Changes found for version ${LIBRARY_VERSION}." 68 | fi 69 | printf "\n" 70 | 71 | inform "Checking for git tag ${LIBRARY_VERSION}..." 72 | git tag -l | grep -E "${LIBRARY_VERSION}$" 73 | if [[ $? -eq 1 ]]; then 74 | warning "Missing git tag for version ${LIBRARY_VERSION}" 75 | fi 76 | printf "\n" 77 | 78 | if [[ $NOPOST ]]; then 79 | inform "Checking for .postN on library version..." 80 | if [[ "$POST_VERSION" != "" ]]; then 81 | warning "Found .$POST_VERSION on library version." 82 | inform "Please only use these for testpypi releases." 83 | exit 1 84 | else 85 | success "OK" 86 | fi 87 | fi 88 | -------------------------------------------------------------------------------- /examples/ltr559.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | sys.path.insert(0, "../") 5 | from i2cdevice import BitField, Device, MockSMBus, Register # noqa: E402 6 | from i2cdevice.adapter import Adapter, LookupAdapter, U16ByteSwapAdapter # noqa: E402 7 | 8 | I2C_ADDR = 0x23 9 | 10 | 11 | class Bit12Adapter(Adapter): 12 | def _encode(self, value): 13 | """ 14 | Convert the 16-bit output into the correct format for reading: 15 | 16 | 0bLLLLLLLLXXXXHHHH -> 0bHHHHLLLLLLLL 17 | """ 18 | return ((value & 0xFF)) << 8 | ((value & 0xF00) >> 8) 19 | 20 | def _decode(self, value): 21 | """ 22 | Convert the 12-bit input into the correct format for the registers, 23 | the low byte followed by 4 empty bits and the high nibble: 24 | 25 | 0bHHHHLLLLLLLL -> 0bLLLLLLLLXXXXHHHH 26 | """ 27 | return ((value & 0xFF00) >> 8) | ((value & 0x000F) << 8) 28 | 29 | 30 | ltr559 = Device(I2C_ADDR, i2c_dev=MockSMBus(0, default_registers={0x86: 0x92}), bit_width=8, registers=( 31 | 32 | Register('ALS_CONTROL', 0x80, fields=( 33 | BitField('gain', 0b00011100, adapter=LookupAdapter({1: 0b000, 2: 0b001, 4: 0b011, 8: 0b011, 48: 0b110, 96: 0b111})), 34 | BitField('sw_reset', 0b00000010), 35 | BitField('mode', 0b00000001) 36 | )), 37 | 38 | Register('PS_CONTROL', 0x81, fields=( 39 | BitField('saturation_indicator_enable', 0b00100000), 40 | BitField('active', 0b00000011, adapter=LookupAdapter({False: 0b00, True: 0b11})) 41 | )), 42 | 43 | Register('PS_LED', 0x82, fields=( 44 | BitField('pulse_freq_khz', 0b11100000, adapter=LookupAdapter({30: 0b000, 40: 0b001, 50: 0b010, 60: 0b011, 70: 0b100, 80: 0b101, 90: 0b110, 100: 0b111})), 45 | BitField('duty_cycle', 0b00011000, adapter=LookupAdapter({0.25: 0b00, 0.5: 0b01, 0.75: 0b10, 1.0: 0b11})), 46 | BitField('current_ma', 0b00000111, adapter=LookupAdapter({5: 0b000, 10: 0b001, 20: 0b010, 50: 0b011, 100: 0b100})) 47 | )), 48 | 49 | Register('PS_N_PULSES', 0x83, fields=( 50 | BitField('count', 0b00001111), 51 | )), 52 | 53 | Register('PS_MEAS_RATE', 0x84, fields=( 54 | BitField('rate_ms', 0b00001111, adapter=LookupAdapter({10: 0b1000, 50: 0b0000, 70: 0b0001, 100: 0b0010, 200: 0b0011, 500: 0b0100, 1000: 0b0101, 2000: 0b0110})), 55 | )), 56 | 57 | Register('ALS_MEAS_RATE', 0x85, fields=( 58 | BitField('integration_time_ms', 0b00111000, adapter=LookupAdapter({100: 0b000, 50: 0b001, 200: 0b010, 400: 0b011, 150: 0b100, 250: 0b101, 300: 0b110, 350: 0b111})), 59 | BitField('repeat_rate_ms', 0b00000111, adapter=LookupAdapter({50: 0b000, 100: 0b001, 200: 0b010, 500: 0b011, 1000: 0b100, 2000: 0b101})) 60 | )), 61 | 62 | Register('PART_ID', 0x86, fields=( 63 | BitField('part_number', 0b11110000), # Should be 0x09H 64 | BitField('revision', 0b00001111) # Should be 0x02H 65 | ), read_only=True, volatile=False), 66 | 67 | Register('MANUFACTURER_ID', 0x87, fields=( 68 | BitField('manufacturer_id', 0b11111111), # Should be 0x05H 69 | ), read_only=True), 70 | 71 | # This will address 0x88, 0x89, 0x8A and 0x8B as a continuous 32bit register 72 | Register('ALS_DATA', 0x88, fields=( 73 | BitField('ch1', 0xFFFF0000, bit_width=16, adapter=U16ByteSwapAdapter()), 74 | BitField('ch0', 0x0000FFFF, bit_width=16, adapter=U16ByteSwapAdapter()) 75 | ), read_only=True, bit_width=32), 76 | 77 | Register('ALS_PS_STATUS', 0x8C, fields=( 78 | BitField('als_data_valid', 0b10000000), 79 | BitField('als_gain', 0b01110000, adapter=LookupAdapter({1: 0b000, 2: 0b001, 4: 0b010, 8: 0b011, 48: 0b110, 96: 0b111})), 80 | BitField('als_interrupt', 0b00001000), # True = Interrupt is active 81 | BitField('als_data', 0b00000100), # True = New data available 82 | BitField('ps_interrupt', 0b00000010), # True = Interrupt is active 83 | BitField('ps_data', 0b00000001) # True = New data available 84 | ), read_only=True), 85 | 86 | # The PS data is actually an 11bit value but since B3 is reserved it'll (probably) read as 0 87 | # We could mask the result if necessary 88 | Register('PS_DATA', 0x8D, fields=( 89 | BitField('ch0', 0xFF0F, adapter=Bit12Adapter()), 90 | BitField('saturation', 0x0080) 91 | ), bit_width=16, read_only=True), 92 | 93 | # INTERRUPT allows the interrupt pin and function behaviour to be configured. 94 | Register('INTERRUPT', 0x8F, fields=( 95 | BitField('polarity', 0b00000100), 96 | BitField('mode', 0b00000011, adapter=LookupAdapter({'off': 0b00, 'ps': 0b01, 'als': 0b10, 'als+ps': 0b11})) 97 | )), 98 | 99 | Register('PS_THRESHOLD', 0x90, fields=( 100 | BitField('upper', 0xFF0F0000, adapter=Bit12Adapter(), bit_width=16), 101 | BitField('lower', 0x0000FF0F, adapter=Bit12Adapter(), bit_width=16) 102 | ), bit_width=32), 103 | 104 | # PS_OFFSET defines the measurement offset value to correct for proximity 105 | # offsets caused by device variations, crosstalk and other environmental factors. 106 | Register('PS_OFFSET', 0x94, fields=( 107 | BitField('offset', 0x03FF), # Last two bits of 0x94, full 8 bits of 0x95 108 | ), bit_width=16), 109 | 110 | # Defines the upper and lower limits of the "ALS" reading. 111 | # An interrupt is triggered if values fall outside of this range. 112 | # See also INTERRUPT_PERSIST. 113 | Register('ALS_THRESHOLD', 0x97, fields=( 114 | BitField('upper', 0xFFFF0000, adapter=U16ByteSwapAdapter(), bit_width=16), 115 | BitField('lower', 0x0000FFFF, adapter=U16ByteSwapAdapter(), bit_width=16) 116 | ), bit_width=32), 117 | 118 | # This register controls how many values must fall outside of the range defined 119 | # by upper and lower threshold limits before the interrupt is asserted. 120 | 121 | # In the case of both "PS" and "ALS", a 0 value indicates that every value outside 122 | # the threshold range should be counted. 123 | # Values therein map to n+1 , ie: 0b0001 requires two consecutive values. 124 | Register('INTERRUPT_PERSIST', 0x9E, fields=( 125 | BitField('PS', 0xF0), 126 | BitField('ALS', 0x0F) 127 | )) 128 | 129 | )) 130 | 131 | 132 | if __name__ == "__main__": 133 | part_id = ltr559.get('PART_ID') 134 | 135 | assert part_id.part_number == 0x09 136 | assert part_id.revision == 0x02 137 | 138 | print(""" 139 | Found LTR-559. 140 | Part ID: 0x{:02x} 141 | Revision: 0x{:02x} 142 | """.format( 143 | part_id.part_number, 144 | part_id.revision 145 | )) 146 | 147 | print(""" 148 | Soft Reset 149 | """) 150 | ltr559.set('ALS_CONTROL', sw_reset=1) 151 | ltr559.set('ALS_CONTROL', sw_reset=0) 152 | try: 153 | while True: 154 | status = ltr559.get('ALS_CONTROL').sw_reset 155 | print("Status: {}".format(status)) 156 | if status == 0: 157 | break 158 | time.sleep(1.0) 159 | except KeyboardInterrupt: 160 | pass 161 | 162 | print("Setting light sensor threshold") 163 | 164 | # The `set` method can handle writes to multiple register fields 165 | # specify each field value as a keyword argument 166 | ltr559.set('ALS_THRESHOLD', 167 | lower=0x0001, 168 | upper=0xFFEE) 169 | 170 | print("{:08x}".format(ltr559.values['ALS_THRESHOLD'])) 171 | 172 | # The `get` method returns register values as an immutable 173 | # namedtuple, and fields will be available as properties 174 | # You must use `set` to write a register. 175 | als_threshold = ltr559.get('ALS_THRESHOLD') 176 | assert als_threshold.lower == 0x0001 177 | assert als_threshold.upper == 0xFFEE 178 | 179 | print("Setting PS threshold") 180 | 181 | ltr559.set('PS_THRESHOLD', 182 | lower=0, 183 | upper=500) 184 | 185 | print("{:08x}".format(ltr559.values['PS_THRESHOLD'])) 186 | 187 | ps_threshold = ltr559.get('PS_THRESHOLD') 188 | assert ps_threshold.lower == 0 189 | assert ps_threshold.upper == 500 190 | 191 | print("Setting integration time and repeat rate") 192 | ltr559.set('PS_MEAS_RATE', rate_ms=100) 193 | ltr559.set('ALS_MEAS_RATE', 194 | integration_time_ms=50, 195 | repeat_rate_ms=50) 196 | 197 | als_meas_rate = ltr559.get('ALS_MEAS_RATE') 198 | assert als_meas_rate.integration_time_ms == 50 199 | assert als_meas_rate.repeat_rate_ms == 50 200 | 201 | print(""" 202 | Activating sensor 203 | """) 204 | 205 | ltr559.set('INTERRUPT', mode='als+ps') 206 | ltr559.set('PS_CONTROL', 207 | active=True, 208 | saturation_indicator_enable=1) 209 | 210 | ltr559.set('PS_LED', 211 | current_ma=50, 212 | duty_cycle=1.0, 213 | pulse_freq_khz=30) 214 | 215 | ltr559.set('PS_N_PULSES', count=1) 216 | 217 | ltr559.set('ALS_CONTROL', 218 | mode=1, 219 | gain=4) 220 | 221 | als_control = ltr559.get('ALS_CONTROL') 222 | assert als_control.mode == 1 223 | assert als_control.gain == 4 224 | 225 | ltr559.set('PS_OFFSET', offset=69) 226 | 227 | als0 = 0 228 | als1 = 0 229 | ps0 = 0 230 | lux = 0 231 | 232 | ch0_c = (17743, 42785, 5926, 0) 233 | ch1_c = (-11059, 19548, -1185, 0) 234 | 235 | try: 236 | while True: 237 | als_ps_status = ltr559.get('ALS_PS_STATUS') 238 | ps_int = als_ps_status.ps_interrupt or als_ps_status.ps_data 239 | als_int = als_ps_status.als_interrupt or als_ps_status.als_data 240 | 241 | if ps_int: 242 | ps0 = ltr559.get('PS_DATA').ch0 243 | 244 | if als_int: 245 | als_data = ltr559.get('ALS_DATA') 246 | als0 = als_data.ch0 247 | als1 = als_data.ch1 248 | 249 | ratio = 1000 250 | if als0 + als0 > 0: 251 | ratio = (als0 * 1000) / (als1 + als0) 252 | 253 | ch_idx = 3 254 | if ratio < 450: 255 | ch_idx = 0 256 | elif ratio < 640: 257 | ch_idx = 1 258 | elif ratio < 850: 259 | ch_idx = 2 260 | 261 | lux = ((als0 * ch0_c[ch_idx]) - (als1 * ch1_c[ch_idx])) / 10000 262 | 263 | print("Lux: {:06.2f}, Light CH0: {:04d}, Light CH1: {:04d}, Proximity: {:04d} New Data LP: 0b{:01d}{:01d}".format(lux, als0, als1, ps0, als_int, ps_int)) 264 | time.sleep(0.05) 265 | except KeyboardInterrupt: 266 | pass 267 | -------------------------------------------------------------------------------- /i2cdevice/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | __version__ = "1.0.0" 4 | 5 | 6 | def _mask_width(value, bit_width=8): 7 | """Get the width of a bitwise mask 8 | 9 | ie: 0b000111 = 3 10 | """ 11 | value >>= _trailing_zeros(value, bit_width) 12 | return value.bit_length() 13 | 14 | 15 | def _leading_zeros(value, bit_width=8): 16 | """Count leading zeros on a binary number with a given bit_width 17 | 18 | ie: 0b0011 = 2 19 | 20 | Used for shifting around values after masking. 21 | """ 22 | count = 0 23 | for _ in range(bit_width): 24 | if value & (1 << (bit_width - 1)): 25 | return count 26 | count += 1 27 | value <<= 1 28 | return count 29 | 30 | 31 | def _trailing_zeros(value, bit_width=8): 32 | """Count trailing zeros on a binary number with a given bit_width 33 | 34 | ie: 0b11000 = 3 35 | 36 | Used for shifting around values after masking. 37 | """ 38 | count = 0 39 | for _ in range(bit_width): 40 | if value & 1: 41 | return count 42 | count += 1 43 | value >>= 1 44 | return count 45 | 46 | 47 | def _int_to_bytes(value, length, endianness='big'): 48 | try: 49 | return value.to_bytes(length, endianness) 50 | except AttributeError: 51 | output = bytearray() 52 | for x in range(length): 53 | offset = x * 8 54 | mask = 0xff << offset 55 | output.append((value & mask) >> offset) 56 | if endianness == 'big': 57 | output.reverse() 58 | return output 59 | 60 | 61 | class MockSMBus: 62 | def __init__(self, i2c_bus, default_registers=None): 63 | self.regs = [0 for _ in range(255)] 64 | if default_registers is not None: 65 | for index in default_registers.keys(): 66 | self.regs[index] = default_registers.get(index) 67 | 68 | def write_i2c_block_data(self, i2c_address, register, values): 69 | self.regs[register:register + len(values)] = values 70 | 71 | def read_i2c_block_data(self, i2c_address, register, length): 72 | return self.regs[register:register + length] 73 | 74 | 75 | class _RegisterProxy(object): 76 | """Register Proxy 77 | 78 | This proxy catches lookups against non existent get_fieldname and set_fieldname methods 79 | and converts them into calls against the device's get_field and set_field methods with 80 | the appropriate options. 81 | 82 | This means device.register.set_field(value) and device.register.get_field(value) will work 83 | and also transparently update the underlying device without the register or field objects 84 | having to know anything about how data is written/read/stored. 85 | 86 | """ 87 | def __init__(self, device, register): 88 | self.device = device 89 | self.register = register 90 | 91 | def __getattribute__(self, name): 92 | if name.startswith("get_"): 93 | name = name.replace("get_", "") 94 | return lambda: self.device.get_field(self.register.name, name) 95 | if name.startswith("set_"): 96 | name = name.replace("set_", "") 97 | return lambda value: self.device.set_field(self.register.name, name, value) 98 | return object.__getattribute__(self, name) 99 | 100 | def write(self): 101 | return self.device.write_register(self.register.name) 102 | 103 | def read(self): 104 | return self.device.read_register(self.register.name) 105 | 106 | def __enter__(self): 107 | self.device.read_register(self.register.name) 108 | self.device.lock_register(self.register.name) 109 | return self 110 | 111 | def __exit__(self, exception_type, exception_value, exception_traceback): 112 | self.device.unlock_register(self.register.name) 113 | 114 | 115 | class Register(): 116 | """Store information about an i2c register""" 117 | def __init__(self, name, address, fields=None, bit_width=8, read_only=False, volatile=True): 118 | self.name = name 119 | self.address = address 120 | self.bit_width = bit_width 121 | self.read_only = read_only 122 | self.volatile = volatile 123 | self.is_read = False 124 | self.fields = {} 125 | 126 | for field in fields: 127 | self.fields[field.name] = field 128 | 129 | self.namedtuple = namedtuple(self.name, sorted(self.fields)) 130 | 131 | 132 | class BitField(): 133 | """Store information about a field or flag in an i2c register""" 134 | def __init__(self, name, mask, adapter=None, bit_width=8, read_only=False): 135 | self.name = name 136 | self.mask = mask 137 | self.adapter = adapter 138 | self.bit_width = bit_width 139 | self.read_only = read_only 140 | 141 | 142 | class BitFlag(BitField): 143 | def __init__(self, name, bit, read_only=False): 144 | BitField.__init__(self, name, 1 << bit, adapter=None, bit_width=8, read_only=read_only) 145 | 146 | 147 | class Device(object): 148 | def __init__(self, i2c_address, i2c_dev=None, bit_width=8, registers=None): 149 | self._bit_width = bit_width 150 | 151 | self.locked = {} 152 | self.registers = {} 153 | self.values = {} 154 | 155 | if isinstance(i2c_address, list): 156 | self._i2c_addresses = i2c_address 157 | self._i2c_address = i2c_address[0] 158 | else: 159 | self._i2c_addresses = [i2c_address] 160 | self._i2c_address = i2c_address 161 | 162 | self._i2c = i2c_dev 163 | 164 | if self._i2c is None: 165 | import smbus2 166 | self._i2c = smbus2.SMBus(1) 167 | 168 | for register in registers: 169 | self.locked[register.name] = False 170 | self.values[register.name] = 0 171 | self.registers[register.name] = register 172 | self.__dict__[register.name] = _RegisterProxy(self, register) 173 | 174 | def lock_register(self, name): 175 | self.locked[name] = True 176 | 177 | def unlock_register(self, name): 178 | self.locked[name] = False 179 | 180 | def read_register(self, name): 181 | register = self.registers[name] 182 | if register.volatile or not register.is_read: 183 | self.values[register.name] = self._i2c_read(register.address, register.bit_width) 184 | register.is_read = True 185 | return self.values[register.name] 186 | 187 | def write_register(self, name): 188 | register = self.registers[name] 189 | return self._i2c_write(register.address, self.values[register.name], register.bit_width) 190 | 191 | def get_addresses(self): 192 | return self._i2c_addresses 193 | 194 | def select_address(self, address): 195 | if address in self._i2c_addresses: 196 | self._i2c_address = address 197 | return True 198 | raise ValueError("Address {:02x} invalid!".format(address)) 199 | 200 | def next_address(self): 201 | next_addr = self._i2c_addresses.index(self._i2c_address) 202 | next_addr += 1 203 | next_addr %= len(self._i2c_addresses) 204 | self._i2c_address = self._i2c_addresses[next_addr] 205 | return self._i2c_address 206 | 207 | def set(self, register, **kwargs): 208 | """Write one or more fields on a device register. 209 | 210 | Accepts multiple keyword arguments, one for each field to write. 211 | 212 | :param register: Name of register to write. 213 | 214 | """ 215 | self.read_register(register) 216 | self.lock_register(register) 217 | for field in kwargs.keys(): 218 | value = kwargs.get(field) 219 | self.set_field(register, field, value) 220 | self.write_register(register) 221 | self.unlock_register(register) 222 | 223 | def get(self, register): 224 | """Get a namedtuple containing register fields. 225 | 226 | :param register: Name of register to retrieve 227 | 228 | """ 229 | result = {} 230 | self.read_register(register) 231 | self.lock_register(register) 232 | for field in self.registers[register].fields: 233 | result[field] = self.get_field(register, field) 234 | self.unlock_register(register) 235 | return self.registers[register].namedtuple(**result) 236 | 237 | def get_field(self, register, field): 238 | register = self.registers[register] 239 | field = register.fields[field] 240 | 241 | if not self.locked[register.name]: 242 | self.read_register(register.name) 243 | 244 | value = self.values[register.name] 245 | 246 | value = (value & field.mask) >> _trailing_zeros(field.mask, register.bit_width) 247 | 248 | if field.adapter is not None: 249 | try: 250 | value = field.adapter._decode(value) 251 | except ValueError as value_error: 252 | raise ValueError("{}: {}".format(field.name, str(value_error))) 253 | 254 | return value 255 | 256 | def set_field(self, register, field, value): 257 | register = self.registers[register] 258 | field = register.fields[field] 259 | shift = _trailing_zeros(field.mask, register.bit_width) 260 | 261 | if field.adapter is not None: 262 | value = field.adapter._encode(value) 263 | 264 | if not self.locked[register.name]: 265 | self.read_register(register.name) 266 | 267 | reg_value = self.values[register.name] 268 | 269 | reg_value &= ~field.mask 270 | reg_value |= (value << shift) & field.mask 271 | 272 | self.values[register.name] = reg_value 273 | 274 | if not self.locked[register.name]: 275 | self.write_register(register.name) 276 | 277 | def get_register(self, register): 278 | register = self.registers[register] 279 | return self._i2c_read(register.address, register.bit_width) 280 | 281 | def _i2c_write(self, register, value, bit_width): 282 | values = _int_to_bytes(value, bit_width // self._bit_width, 'big') 283 | values = list(values) 284 | self._i2c.write_i2c_block_data(self._i2c_address, register, values) 285 | 286 | def _i2c_read(self, register, bit_width): 287 | value = 0 288 | for x in self._i2c.read_i2c_block_data(self._i2c_address, register, bit_width // self._bit_width): 289 | value <<= 8 290 | value |= x 291 | return value 292 | -------------------------------------------------------------------------------- /i2cdevice/adapter.py: -------------------------------------------------------------------------------- 1 | class Adapter: 2 | """ 3 | Must implement `_decode()` and `_encode()`. 4 | """ 5 | def _decode(self, value): 6 | raise NotImplementedError 7 | 8 | def _encode(self, value): 9 | raise NotImplementedError 10 | 11 | 12 | class LookupAdapter(Adapter): 13 | """Adaptor with a dictionary of values. 14 | 15 | :param lookup_table: A dictionary of one or more key/value pairs where the key is the human-readable value and the value is the bitwise register value 16 | 17 | """ 18 | def __init__(self, lookup_table, snap=True): 19 | self.lookup_table = lookup_table 20 | self.snap = snap 21 | 22 | def _decode(self, value): 23 | for k, v in self.lookup_table.items(): 24 | if v == value: 25 | return k 26 | raise ValueError("{} not in lookup table".format(value)) 27 | 28 | def _encode(self, value): 29 | if self.snap and type(value) in [int, float]: 30 | value = min(list(self.lookup_table.keys()), key=lambda x: abs(x - value)) 31 | return self.lookup_table[value] 32 | 33 | 34 | class U16ByteSwapAdapter(Adapter): 35 | """Adaptor to swap the bytes in a 16bit integer.""" 36 | def _byteswap(self, value): 37 | return (value >> 8) | ((value & 0xFF) << 8) 38 | 39 | def _decode(self, value): 40 | return self._byteswap(value) 41 | 42 | def _encode(self, value): 43 | return self._byteswap(value) 44 | -------------------------------------------------------------------------------- /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=/boot/config.txt 4 | DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` 5 | CONFIG_BACKUP=false 6 | APT_HAS_UPDATED=false 7 | RESOURCES_TOP_DIR=$HOME/Pimoroni 8 | VENV_BASH_SNIPPET=$RESOURCES_DIR/auto_venv.sh 9 | VENV_DIR=$HOME/.virtualenvs/pimoroni 10 | WD=`pwd` 11 | USAGE="./install.sh (--unstable)" 12 | POSITIONAL_ARGS=() 13 | FORCE=false 14 | UNSTABLE=false 15 | PYTHON="python" 16 | 17 | 18 | user_check() { 19 | if [ $(id -u) -eq 0 ]; then 20 | printf "Script should not be run as root. Try './install.sh'\n" 21 | exit 1 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 | prompt() { 39 | read -r -p "$1 [y/N] " response < /dev/tty 40 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 41 | true 42 | else 43 | false 44 | fi 45 | } 46 | 47 | success() { 48 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 49 | } 50 | 51 | inform() { 52 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 53 | } 54 | 55 | warning() { 56 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 57 | } 58 | 59 | venv_bash_snippet() { 60 | if [ ! -f $VENV_BASH_SNIPPET ]; then 61 | cat << EOF > $VENV_BASH_SNIPPET 62 | # Add `source $RESOURCES_DIR/auto_venv.sh` to your ~/.bashrc to activate 63 | # the Pimoroni virtual environment automagically! 64 | VENV_DIR="$VENV_DIR" 65 | if [ ! -f \$VENV_DIR/bin/activate ]; then 66 | printf "Creating user Python environment in \$VENV_DIR, please wait...\n" 67 | mkdir -p \$VENV_DIR 68 | python3 -m venv --system-site-packages \$VENV_DIR 69 | fi 70 | printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" 71 | source \$VENV_DIR/bin/activate 72 | EOF 73 | fi 74 | } 75 | 76 | venv_check() { 77 | PYTHON_BIN=`which $PYTHON` 78 | if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then 79 | printf "This script should be run in a virtual Python environment.\n" 80 | if confirm "Would you like us to create one for you?"; then 81 | if [ ! -f $VENV_DIR/bin/activate ]; then 82 | inform "Creating virtual Python environment in $VENV_DIR, please wait...\n" 83 | mkdir -p $VENV_DIR 84 | /usr/bin/python3 -m venv $VENV_DIR --system-site-packages 85 | venv_bash_snippet 86 | else 87 | inform "Found existing virtual Python environment in $VENV_DIR\n" 88 | fi 89 | inform "Activating virtual Python environment in $VENV_DIR..." 90 | inform "source $VENV_DIR/bin/activate\n" 91 | source $VENV_DIR/bin/activate 92 | 93 | else 94 | exit 1 95 | fi 96 | fi 97 | } 98 | 99 | function do_config_backup { 100 | if [ ! $CONFIG_BACKUP == true ]; then 101 | CONFIG_BACKUP=true 102 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 103 | inform "Backing up $CONFIG to /boot/$FILENAME\n" 104 | sudo cp $CONFIG /boot/$FILENAME 105 | mkdir -p $RESOURCES_TOP_DIR/config-backups/ 106 | cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME 107 | if [ -f "$UNINSTALLER" ]; then 108 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER 109 | fi 110 | fi 111 | } 112 | 113 | function apt_pkg_install { 114 | PACKAGES=() 115 | PACKAGES_IN=("$@") 116 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 117 | PACKAGE="${PACKAGES_IN[$i]}" 118 | if [ "$PACKAGE" == "" ]; then continue; fi 119 | printf "Checking for $PACKAGE\n" 120 | dpkg -L $PACKAGE > /dev/null 2>&1 121 | if [ "$?" == "1" ]; then 122 | PACKAGES+=("$PACKAGE") 123 | fi 124 | done 125 | PACKAGES="${PACKAGES[@]}" 126 | if ! [ "$PACKAGES" == "" ]; then 127 | echo "Installing missing packages: $PACKAGES" 128 | if [ ! $APT_HAS_UPDATED ]; then 129 | sudo apt update 130 | APT_HAS_UPDATED=true 131 | fi 132 | sudo apt install -y $PACKAGES 133 | if [ -f "$UNINSTALLER" ]; then 134 | echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER 135 | fi 136 | fi 137 | } 138 | 139 | function pip_pkg_install { 140 | PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" 141 | } 142 | 143 | while [[ $# -gt 0 ]]; do 144 | K="$1" 145 | case $K in 146 | -u|--unstable) 147 | UNSTABLE=true 148 | shift 149 | ;; 150 | -f|--force) 151 | FORCE=true 152 | shift 153 | ;; 154 | -p|--python) 155 | PYTHON=$2 156 | shift 157 | shift 158 | ;; 159 | *) 160 | if [[ $1 == -* ]]; then 161 | printf "Unrecognised option: $1\n"; 162 | printf "Usage: $USAGE\n"; 163 | exit 1 164 | fi 165 | POSITIONAL_ARGS+=("$1") 166 | shift 167 | esac 168 | done 169 | 170 | user_check 171 | venv_check 172 | 173 | if [ ! -f `which $PYTHON` ]; then 174 | printf "Python path $PYTHON not found!\n" 175 | exit 1 176 | fi 177 | 178 | PYTHON_VER=`$PYTHON --version` 179 | 180 | printf "$LIBRARY_NAME Python Library: Installer\n\n" 181 | 182 | inform "Checking Dependencies. Please wait..." 183 | 184 | pip_pkg_install toml 185 | 186 | CONFIG_VARS=`$PYTHON - < $UNINSTALLER 221 | printf "It's recommended you run these steps manually.\n" 222 | printf "If you want to run the full script, open it in\n" 223 | printf "an editor and remove 'exit 1' from below.\n" 224 | exit 1 225 | source $VIRTUAL_ENV/bin/activate 226 | EOF 227 | 228 | if $UNSTABLE; then 229 | warning "Installing unstable library from source.\n\n" 230 | else 231 | printf "Installing stable library from pypi.\n\n" 232 | fi 233 | 234 | inform "Installing for $PYTHON_VER...\n" 235 | apt_pkg_install "${APT_PACKAGES[@]}" 236 | if $UNSTABLE; then 237 | pip_pkg_install . 238 | else 239 | pip_pkg_install $LIBRARY_NAME 240 | fi 241 | if [ $? -eq 0 ]; then 242 | success "Done!\n" 243 | echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER 244 | fi 245 | 246 | cd $WD 247 | 248 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 249 | CMD="${SETUP_CMDS[$i]}" 250 | # Attempt to catch anything that touches /boot/config.txt and trigger a backup 251 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then 252 | do_config_backup 253 | fi 254 | eval $CMD 255 | done 256 | 257 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 258 | CONFIG_LINE="${CONFIG_TXT[$i]}" 259 | if ! [ "$CONFIG_LINE" == "" ]; then 260 | do_config_backup 261 | inform "Adding $CONFIG_LINE to $CONFIG\n" 262 | sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG 263 | if ! grep -q "^$CONFIG_LINE" $CONFIG; then 264 | printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG 265 | fi 266 | fi 267 | done 268 | 269 | if [ -d "examples" ]; then 270 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 271 | inform "Copying examples to $RESOURCES_DIR" 272 | cp -r examples/ $RESOURCES_DIR 273 | echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER 274 | success "Done!" 275 | fi 276 | fi 277 | 278 | printf "\n" 279 | 280 | if confirm "Would you like to generate documentation?"; then 281 | pip_pkg_install pdoc 282 | printf "Generating documentation.\n" 283 | $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null 284 | if [ $? -eq 0 ]; then 285 | inform "Documentation saved to $RESOURCES_DIR/docs" 286 | success "Done!" 287 | else 288 | warning "Error: Failed to generate documentation." 289 | fi 290 | fi 291 | 292 | success "\nAll done!" 293 | inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" 294 | inform "Find uninstall steps in $UNINSTALLER\n" 295 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-fancy-pypi-readme"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "i2cdevice" 7 | dynamic = ["version", "readme"] 8 | description = "Python framework for interacting with SMBus-compatible i2c devices" 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 | "SMBus", 21 | "i2c" 22 | ] 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Intended Audience :: Developers", 26 | "License :: OSI Approved :: MIT License", 27 | "Operating System :: POSIX :: Linux", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3 :: Only", 35 | "Topic :: Software Development", 36 | "Topic :: Software Development :: Libraries", 37 | "Topic :: System :: Hardware", 38 | ] 39 | dependencies = [ 40 | "smbus2" 41 | ] 42 | 43 | [project.urls] 44 | GitHub = "https://www.github.com/pimoroni/i2cdevice-python" 45 | Homepage = "https://www.pimoroni.com" 46 | 47 | [tool.hatch.version] 48 | path = "i2cdevice/__init__.py" 49 | 50 | [tool.hatch.build] 51 | include = [ 52 | "i2cdevice", 53 | "README.md", 54 | "CHANGELOG.md", 55 | "LICENSE" 56 | ] 57 | 58 | [tool.hatch.build.targets.sdist] 59 | include = [ 60 | "*" 61 | ] 62 | exclude = [ 63 | ".*", 64 | "dist" 65 | ] 66 | 67 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 68 | content-type = "text/markdown" 69 | fragments = [ 70 | { path = "README.md" }, 71 | { text = "\n" }, 72 | { path = "CHANGELOG.md" } 73 | ] 74 | 75 | [tool.ruff] 76 | exclude = [ 77 | '.tox', 78 | '.egg', 79 | '.git', 80 | '__pycache__', 81 | 'build', 82 | 'dist' 83 | ] 84 | line-length = 200 85 | 86 | [tool.codespell] 87 | skip = """ 88 | ./.tox,\ 89 | ./.egg,\ 90 | ./.git,\ 91 | ./__pycache__,\ 92 | ./build,\ 93 | ./dist.\ 94 | """ 95 | ignore-words-list = "als" 96 | 97 | [tool.isort] 98 | line_length = 200 99 | 100 | [tool.check-manifest] 101 | ignore = [ 102 | '.stickler.yml', 103 | 'boilerplate.md', 104 | 'check.sh', 105 | 'install.sh', 106 | 'uninstall.sh', 107 | 'Makefile', 108 | 'tox.ini', 109 | 'tests/*', 110 | 'examples/*', 111 | '.coveragerc', 112 | 'requirements-dev.txt' 113 | ] 114 | 115 | [tool.pimoroni] 116 | apt_packages = [] 117 | configtxt = [] 118 | commands = [] 119 | -------------------------------------------------------------------------------- /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/test_adapters.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from i2cdevice.adapter import Adapter, LookupAdapter, U16ByteSwapAdapter 4 | 5 | 6 | def test_adaptor_class(): 7 | adapter = Adapter() 8 | with pytest.raises(NotImplementedError): 9 | adapter._decode(0) 10 | with pytest.raises(NotImplementedError): 11 | adapter._encode(0) 12 | 13 | 14 | def test_lookup_adapter(): 15 | adapter = LookupAdapter({'Zero': 0, 'One': 1}) 16 | assert adapter._decode(0) == 'Zero' 17 | assert adapter._decode(1) == 'One' 18 | with pytest.raises(ValueError): 19 | adapter._decode(2) 20 | with pytest.raises(KeyError): 21 | adapter._encode('Two') 22 | 23 | 24 | def test_lookup_adapter_snap(): 25 | adapter = LookupAdapter({0: 0, 1: 1}) 26 | assert adapter._encode(0) == 0 27 | assert adapter._encode(1) == 1 28 | assert adapter._encode(0.1) == 0 29 | assert adapter._encode(0.9) == 1 30 | 31 | adapter = LookupAdapter({0: 0, 1: 0}, snap=False) 32 | with pytest.raises(KeyError): 33 | adapter._encode(0.1) 34 | 35 | 36 | def test_byteswap_adapter(): 37 | adapter = U16ByteSwapAdapter() 38 | assert adapter._encode(0xFF00) == 0x00FF 39 | assert adapter._decode(0x00FF) == 0xFF00 40 | -------------------------------------------------------------------------------- /tests/test_defaultbus.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from i2cdevice import BitField, Device, MockSMBus, Register 4 | 5 | 6 | class SMBus(): 7 | SMBus = MockSMBus 8 | 9 | 10 | def test_smbus_io(): 11 | sys.modules['smbus2'] = SMBus 12 | device = Device(0x00, i2c_dev=None, registers=( 13 | Register('test', 0x00, fields=( 14 | BitField('test', 0xFF), 15 | )), 16 | )) 17 | del device 18 | -------------------------------------------------------------------------------- /tests/test_device.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from i2cdevice import BitField, BitFlag, Device, MockSMBus, Register 4 | from i2cdevice.adapter import U16ByteSwapAdapter 5 | 6 | 7 | def test_register_locking(): 8 | bus = MockSMBus(1) 9 | device = Device(0x00, i2c_dev=bus, registers=( 10 | Register('test', 0x00, fields=( 11 | BitField('test', 0xFF), 12 | )), 13 | )) 14 | 15 | device.test.set_test(77) 16 | device.lock_register('test') 17 | 18 | bus.regs[0] = 11 19 | assert device.test.get_test() == 77 20 | 21 | device.unlock_register('test') 22 | assert device.test.get_test() == 11 23 | 24 | 25 | def test_adapters(): 26 | bus = MockSMBus(1) 27 | device = Device(0x00, i2c_dev=bus, registers=( 28 | Register('adapter', 0x01, fields=( 29 | BitField('test', 0xFFFF, adapter=U16ByteSwapAdapter()), 30 | )), 31 | )) 32 | 33 | device.adapter.set_test(0xFF00) 34 | 35 | assert device.adapter.get_test() == 0xFF00 36 | 37 | assert bus.regs[0:2] == [0x00, 0xFF] 38 | 39 | 40 | def test_address_select(): 41 | bus = MockSMBus(1) 42 | device = Device([0x00, 0x01], i2c_dev=bus, registers=( 43 | Register('test', 0x00, fields=( 44 | BitField('test', 0xFF), 45 | )), 46 | )) 47 | 48 | assert device.get_addresses() == [0x00, 0x01] 49 | assert device.select_address(0x01) is True 50 | with pytest.raises(ValueError): 51 | device.select_address(0x02) 52 | 53 | assert device.next_address() == 0x00 54 | assert device.next_address() == 0x01 55 | 56 | 57 | def test_get_set_field(): 58 | bus = MockSMBus(1) 59 | device = Device([0x00, 0x01], i2c_dev=bus, registers=( 60 | Register('test', 0x00, fields=( 61 | BitField('test', 0xFF), 62 | )), 63 | )) 64 | 65 | device.set_field('test', 'test', 99) 66 | 67 | assert device.get_field('test', 'test') == 99 68 | 69 | assert bus.regs[0] == 99 70 | 71 | 72 | def test_get_set_field_overflow(): 73 | bus = MockSMBus(1) 74 | device = Device([0x00, 0x01], i2c_dev=bus, registers=( 75 | Register('test', 0x00, fields=( 76 | BitField('test', 0xFF), 77 | )), 78 | )) 79 | 80 | device.set_field('test', 'test', 9999999) 81 | 82 | assert device.get_field('test', 'test') == 127 83 | 84 | assert bus.regs[0] == 127 85 | 86 | 87 | def test_bitflag(): 88 | bus = MockSMBus(1) 89 | device = Device([0x00, 0x01], i2c_dev=bus, registers=( 90 | Register('test', 0x00, fields=( 91 | BitFlag('test', 6), # Sixth bit from the right 92 | )), 93 | )) 94 | 95 | device.test.set_test(True) 96 | 97 | assert bus.regs[0] == 0b01000000 98 | 99 | device.test.set_test(False) 100 | 101 | assert bus.regs[0] == 0b00000000 102 | 103 | 104 | def test_get_register(): 105 | bus = MockSMBus(1) 106 | bus.regs[0:3] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF] 107 | device = Device([0x00, 0x01], i2c_dev=bus, registers=( 108 | Register('test24', 0x00, fields=( 109 | BitField('test', 0xFFF), 110 | ), bit_width=24), 111 | Register('test32', 0x00, fields=( 112 | BitField('test', 0xFFF), 113 | ), bit_width=32), 114 | Register('test48', 0x00, fields=( 115 | BitField('test', 0xFFF), 116 | ), bit_width=48), 117 | )) 118 | 119 | assert device.get_register('test24') == 0xAABBCC 120 | 121 | assert device.get_register('test32') == 0xAABBCCDD 122 | 123 | assert device.get_register('test48') == 0xAABBCCDDEEFF 124 | 125 | 126 | def test_missing_regiser(): 127 | bus = MockSMBus(1) 128 | device = Device([0x00, 0x01], i2c_dev=bus, registers=( 129 | Register('test', 0x00, fields=( 130 | BitFlag('test', 6), # Sixth bit from the right 131 | )), 132 | )) 133 | 134 | with pytest.raises(KeyError): 135 | device.get_register('foo') 136 | -------------------------------------------------------------------------------- /tests/test_mocksmbus.py: -------------------------------------------------------------------------------- 1 | from i2cdevice import MockSMBus 2 | 3 | 4 | def test_smbus_io(): 5 | bus = MockSMBus(1) 6 | bus.write_i2c_block_data(0x00, 0x00, [0xff, 0x00, 0xff]) 7 | assert bus.read_i2c_block_data(0x00, 0x00, 3) == [0xff, 0x00, 0xff] 8 | 9 | 10 | def test_smbus_default_regs(): 11 | bus = MockSMBus(1, default_registers={0x60: 0x99, 0x88: 0x51}) 12 | assert bus.read_i2c_block_data(0x00, 0x60, 1) == [0x99] 13 | assert bus.read_i2c_block_data(0x00, 0x88, 1) == [0x51] 14 | -------------------------------------------------------------------------------- /tests/test_registerproxy.py: -------------------------------------------------------------------------------- 1 | from i2cdevice import BitField, Device, MockSMBus, Register 2 | 3 | 4 | def test_register_proxy(): 5 | """This API pattern has been deprecated in favour of set/get.""" 6 | bus = MockSMBus(1) 7 | device = Device(0x00, i2c_dev=bus, registers=( 8 | Register('test', 0x00, fields=( 9 | BitField('test', 0xFF), 10 | )), 11 | )) 12 | device.test.set_test(123) 13 | 14 | assert device.test.get_test() == 123 15 | 16 | assert bus.regs[0] == 123 17 | 18 | with device.test as test: 19 | test.set_test(77) 20 | test.write() 21 | 22 | assert device.test.get_test() == 77 23 | 24 | assert bus.regs[0] == 77 25 | 26 | assert device.test.read() == 77 27 | -------------------------------------------------------------------------------- /tests/test_set_and_get.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from i2cdevice import BitField, Device, MockSMBus, Register 4 | from i2cdevice.adapter import LookupAdapter 5 | 6 | 7 | def test_set_regs(): 8 | bus = MockSMBus(1) 9 | device = Device(0x00, i2c_dev=bus, registers=( 10 | Register('test', 0x00, fields=( 11 | BitField('test', 0xFF), 12 | )), 13 | )) 14 | device.set('test', test=123) 15 | 16 | assert device.get('test').test == 123 17 | 18 | assert bus.regs[0] == 123 19 | 20 | 21 | def test_get_regs(): 22 | bus = MockSMBus(1) 23 | device = Device(0x00, i2c_dev=bus, registers=( 24 | Register('test', 0x00, fields=( 25 | BitField('test', 0xFF00), 26 | BitField('monkey', 0x00FF), 27 | ), bit_width=16), 28 | )) 29 | device.set('test', test=0x66, monkey=0x77) 30 | 31 | reg = device.get('test') 32 | reg.test == 0x66 33 | reg.monkey == 0x77 34 | 35 | assert bus.regs[0] == 0x66 36 | assert bus.regs[1] == 0x77 37 | 38 | 39 | def test_field_name_in_adapter_error(): 40 | bus = MockSMBus(1) 41 | device = Device(0x00, i2c_dev=bus, registers=( 42 | Register('test', 0x00, fields=( 43 | BitField('test', 0xFF00, adapter=LookupAdapter({'x': 1})), 44 | ), bit_width=16), 45 | )) 46 | 47 | with pytest.raises(ValueError) as e: 48 | reg = device.get('test') 49 | assert 'test' in e 50 | del reg 51 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from i2cdevice import _int_to_bytes, _leading_zeros, _mask_width, _trailing_zeros 4 | 5 | 6 | def test_mask_width(): 7 | assert _mask_width(0b111) == 3 8 | assert _mask_width(0b101) == 3 9 | assert _mask_width(0b0111) == 3 10 | assert _mask_width(0b1110) == 3 11 | 12 | 13 | def test_leading_zeros(): 14 | assert _leading_zeros(0b1) == 7 15 | assert _leading_zeros(0b10) == 6 16 | assert _leading_zeros(0b100) == 5 17 | assert _leading_zeros(0b100000000) == 8 # 9nth bit not counted by default 18 | 19 | 20 | def test_trailing_zeros(): 21 | assert _trailing_zeros(0b1) == 0 22 | assert _trailing_zeros(0b10) == 1 23 | assert _trailing_zeros(0b100) == 2 24 | assert _trailing_zeros(0b00000000) == 8 # Mask is all zeros 25 | 26 | 27 | def test_int_to_bytes(): 28 | assert _int_to_bytes(512, 2) == b'\x02\x00' 29 | assert _int_to_bytes(512, 2, endianness='little') == b'\x00\x02' 30 | assert _int_to_bytes(512, 2, endianness='big') == b'\x02\x00' 31 | 32 | with pytest.raises(TypeError): 33 | _int_to_bytes('', 2) 34 | -------------------------------------------------------------------------------- /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 . 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 "$LIBRARY_NAME Python Library: Uninstaller\n\n" 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 | --------------------------------------------------------------------------------