├── .coveragerc ├── .github └── workflows │ ├── build.yml │ ├── qa.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── bmp280 └── __init__.py ├── check.sh ├── examples ├── compensated-temperature.py ├── dump-calibration.py ├── relative-altitude.py ├── temperature-and-pressure.py └── temperature-forced-mode.py ├── install.sh ├── pyproject.toml ├── requirements-dev.txt ├── test.py ├── tests ├── calibration.py ├── test_compensation.py ├── test_setup.py └── tools.py ├── tox.ini └── uninstall.sh /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = bmp280 3 | omit = 4 | .tox/* 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | ----- 3 | 4 | * Repackage to hatch/pyproject.toml 5 | 6 | 0.0.4 7 | ----- 8 | 9 | * Add support for forced-mode on demand i2c 10 | * Change altitude formula 11 | * Allow manual temperature compensation for altitude 12 | * Allow oversampling settings to be configured on `__init__` 13 | 14 | 0.0.3 15 | ----- 16 | 17 | * Migrate to i2cdevice>=0.0.6 set/get API 18 | 19 | 0.0.2 20 | ----- 21 | 22 | * Added `get_altitude` method 23 | * Corrected pressure to hPa 24 | 25 | 0.0.1 26 | ----- 27 | 28 | * Initial Release 29 | -------------------------------------------------------------------------------- /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.md 4 | include setup.py 5 | recursive-include bmp280 *.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 | 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 | # BMP280 Temperature, Pressure, & Altitude Sensor 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bmp280-python/test.yml?branch=main)](https://github.com/pimoroni/bmp280-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/bmp280-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bmp280-python?branch=main) 5 | [![PyPi Package](https://img.shields.io/pypi/v/bmp280.svg)](https://pypi.python.org/pypi/bmp280) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/bmp280.svg)](https://pypi.python.org/pypi/bmp280) 7 | 8 | Suitable for measuring ambient temperature, barometric pressure, and altitude, the BMP280 is a basic weather sensor. 9 | 10 | # Installing 11 | 12 | Stable library from PyPi: 13 | 14 | * Just run `pip install bmp280` 15 | 16 | Latest/development library from GitHub: 17 | 18 | * `git clone https://github.com/pimoroni/bmp280-python` 19 | * `cd bmp280-python` 20 | * `./install.sh --unstable` 21 | 22 | -------------------------------------------------------------------------------- /bmp280/__init__.py: -------------------------------------------------------------------------------- 1 | """BMP280 Driver.""" 2 | 3 | import struct 4 | import time 5 | 6 | from i2cdevice import BitField, Device, Register, _int_to_bytes 7 | from i2cdevice.adapter import Adapter, LookupAdapter 8 | 9 | __version__ = "1.0.0" 10 | 11 | CHIP_ID = 0x58 12 | I2C_ADDRESS_GND = 0x76 13 | I2C_ADDRESS_VCC = 0x77 14 | 15 | 16 | class S16Adapter(Adapter): 17 | """Convert unsigned 16bit integer to signed.""" 18 | 19 | def _decode(self, value): 20 | return struct.unpack(" /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/compensated-temperature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | from subprocess import PIPE, Popen 5 | 6 | from smbus2 import SMBus 7 | 8 | from bmp280 import BMP280 9 | 10 | print("""compensated-temperature.py - Use the CPU temperature to compensate temperature 11 | readings from the BMP280 sensor. Method adapted from Initial State's Enviro pHAT 12 | review: https://medium.com/@InitialState/tutorial-review-enviro-phat-for-raspberry-pi-4cd6d8c63441 13 | 14 | Press Ctrl+C to exit! 15 | 16 | """) 17 | 18 | # Initialise the BMP280 19 | bus = SMBus(1) 20 | bmp280 = BMP280(i2c_dev=bus) 21 | 22 | 23 | # Gets the CPU temperature in degrees C 24 | def get_cpu_temperature(): 25 | process = Popen(["vcgencmd", "measure_temp"], stdout=PIPE) 26 | output, _error = process.communicate() 27 | return float(output[output.index("=") + 1 : output.rindex("'")]) 28 | 29 | 30 | factor = 1.2 # Smaller numbers adjust temp down, vice versa 31 | smooth_size = 10 # Dampens jitter due to rapid CPU temp changes 32 | 33 | cpu_temps = [] 34 | 35 | while True: 36 | cpu_temp = get_cpu_temperature() 37 | cpu_temps.append(cpu_temp) 38 | 39 | if len(cpu_temps) > smooth_size: 40 | cpu_temps = cpu_temps[1:] 41 | 42 | smoothed_cpu_temp = sum(cpu_temps) / float(len(cpu_temps)) 43 | raw_temp = bmp280.get_temperature() 44 | comp_temp = raw_temp - ((smoothed_cpu_temp - raw_temp) / factor) 45 | 46 | print(f"Compensated temperature: {comp_temp:05.2f} *C") 47 | 48 | time.sleep(1.0) 49 | -------------------------------------------------------------------------------- /examples/dump-calibration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from smbus2 import SMBus 4 | 5 | from bmp280 import BMP280 6 | 7 | print("""dump-calibration.py - Dumps calibration data. 8 | 9 | Press Ctrl+C to exit! 10 | 11 | """) 12 | 13 | # Initialise the BMP280 14 | bmp280 = BMP280(i2c_dev=SMBus(1)) 15 | bmp280.setup() 16 | 17 | for key in dir(bmp280.calibration): 18 | if key.startswith("dig_"): 19 | value = getattr(bmp280.calibration, key) 20 | print(f"{key} = {value}") 21 | -------------------------------------------------------------------------------- /examples/relative-altitude.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from smbus2 import SMBus 6 | 7 | from bmp280 import BMP280 8 | 9 | print("""relative-altitude.py - Calculates relative altitude from pressure. 10 | 11 | Press Ctrl+C to exit! 12 | 13 | """) 14 | 15 | # Initialise the BMP280 16 | bus = SMBus(1) 17 | bmp280 = BMP280(i2c_dev=bus) 18 | 19 | baseline_values = [] 20 | baseline_size = 100 21 | 22 | print(f"Collecting baseline values for {baseline_size:d} seconds. Do not move the sensor!\n") 23 | 24 | for i in range(baseline_size): 25 | pressure = bmp280.get_pressure() 26 | baseline_values.append(pressure) 27 | time.sleep(1) 28 | 29 | baseline = sum(baseline_values[:-25]) / len(baseline_values[:-25]) 30 | 31 | while True: 32 | altitude = bmp280.get_altitude(qnh=baseline) 33 | print(f"Relative altitude: {altitude:05.2f} metres") 34 | time.sleep(1) 35 | -------------------------------------------------------------------------------- /examples/temperature-and-pressure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | 5 | from smbus2 import SMBus 6 | 7 | from bmp280 import BMP280 8 | 9 | print("""temperature-and-pressure.py - Displays the temperature and pressure. 10 | 11 | Press Ctrl+C to exit! 12 | 13 | """) 14 | 15 | # Initialise the BMP280 16 | bus = SMBus(1) 17 | bmp280 = BMP280(i2c_dev=bus) 18 | 19 | while True: 20 | temperature = bmp280.get_temperature() 21 | pressure = bmp280.get_pressure() 22 | print(f"{temperature:05.2f}*C {pressure:05.2f}hPa") 23 | time.sleep(1) 24 | -------------------------------------------------------------------------------- /examples/temperature-forced-mode.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from smbus2 import SMBus 4 | 5 | from bmp280 import BMP280 6 | 7 | bus = SMBus(1) 8 | bmp280 = BMP280(i2c_dev=bus) 9 | 10 | # Set up in "forced" mode 11 | # In this mode `get_temperature` and `get_pressure` will trigger 12 | # a new reading and wait for the result. 13 | # The chip will return to sleep mode when finished. 14 | bmp280.setup(mode="forced") 15 | 16 | while True: 17 | temperature = bmp280.get_temperature() 18 | print(f"{temperature:05.2f}*C") 19 | time.sleep(1) 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-fancy-pypi-readme"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "bmp280" 7 | dynamic = ["version", "readme"] 8 | description = "Python library for the BMP280 temperature and pressure 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/bmp280-python" 43 | Homepage = "https://www.pimoroni.com" 44 | 45 | [tool.hatch.version] 46 | path = "bmp280/__init__.py" 47 | 48 | [tool.hatch.build] 49 | include = [ 50 | "bmp280", 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from bmp280 import CHIP_ID, _bmp280 4 | 5 | print('0x{:02x} {}'.format(_bmp280.CHIP_ID.get_id(), 'ok' if _bmp280.CHIP_ID.get_id() == CHIP_ID else 'err')) 6 | 7 | with _bmp280.CTRL_MEAS as reg: 8 | reg.set_mode('normal') 9 | reg.set_osrs_t(16) # 16x temperature oversampling 10 | reg.set_osrs_p(16) # 16x pressure oversampling 11 | reg.write() 12 | 13 | with _bmp280.CONFIG as reg: 14 | reg.set_t_sb(500) 15 | reg.set_filter(2) 16 | reg.write() 17 | 18 | with _bmp280.CALIBRATION as reg: 19 | dig_t1 = reg.get_dig_t1() 20 | dig_t2 = reg.get_dig_t2() 21 | dig_t3 = reg.get_dig_t3() 22 | 23 | dig_p1 = reg.get_dig_p1() 24 | dig_p2 = reg.get_dig_p2() 25 | dig_p3 = reg.get_dig_p3() 26 | dig_p4 = reg.get_dig_p4() 27 | dig_p5 = reg.get_dig_p5() 28 | dig_p6 = reg.get_dig_p6() 29 | dig_p7 = reg.get_dig_p7() 30 | dig_p8 = reg.get_dig_p8() 31 | dig_p9 = reg.get_dig_p9() 32 | 33 | print(dig_t1, dig_t2, dig_t3) 34 | print(dig_p1, dig_p2, dig_p3, dig_p4, dig_p5, dig_p6, dig_p7, dig_p8, dig_p9) 35 | 36 | try: 37 | while True: 38 | raw_temp = _bmp280.DATA.get_temperature() 39 | raw_pres = _bmp280.DATA.get_pressure() 40 | 41 | print('0b{:020b}'.format(raw_temp)) 42 | 43 | var1 = (raw_temp / 16384.0 - dig_t1 / 1024.0) * dig_t2 44 | var2 = (raw_temp / 131072.0 - dig_t1 / 8192.0) * (raw_temp / 131072.0 - dig_t1 / 8192.0) * dig_t3 45 | temp = (var1 + var2) / 5120.0 46 | t_fine = (var1 + var2) 47 | 48 | print(temp) 49 | time.sleep(1) 50 | 51 | except KeyboardInterrupt: 52 | pass 53 | -------------------------------------------------------------------------------- /tests/calibration.py: -------------------------------------------------------------------------------- 1 | import bmp280 2 | 3 | 4 | class BMP280Calibration(bmp280.BMP280Calibration): 5 | """Prefil the calibration class with known values.""" 6 | def __init__(self): 7 | bmp280.BMP280Calibration.__init__(self) 8 | 9 | self.dig_t1 = 28009 10 | self.dig_t2 = 25654 11 | self.dig_t3 = 50 12 | 13 | self.dig_p1 = 39145 14 | self.dig_p2 = -10750 15 | self.dig_p3 = 3024 16 | self.dig_p4 = 5667 17 | self.dig_p5 = -120 18 | self.dig_p6 = -7 19 | self.dig_p7 = 15500 20 | self.dig_p8 = -14600 21 | self.dig_p9 = 6000 22 | -------------------------------------------------------------------------------- /tests/test_compensation.py: -------------------------------------------------------------------------------- 1 | TEST_TEMP_RAW = 529191 2 | TEST_TEMP_CMP = 24.7894877676 3 | 4 | TEST_PRES_RAW = 326816 5 | TEST_PRES_CMP = 1006.61517564 6 | TEST_ALT_CMP = 57.3174 7 | 8 | 9 | def test_temperature(): 10 | from calibration import BMP280Calibration 11 | from tools import SMBusFakeDevice 12 | 13 | from bmp280 import BMP280 14 | dev = SMBusFakeDevice(1) 15 | 16 | # Load the fake temperature into the virtual registers 17 | dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 18 | dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 19 | dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 20 | 21 | bmp280 = BMP280(i2c_dev=dev) 22 | bmp280.setup() 23 | 24 | # Replace the loaded calibration with our known values 25 | bmp280.calibration = BMP280Calibration() 26 | 27 | assert round(bmp280.get_temperature(), 4) == round(TEST_TEMP_CMP, 4) 28 | 29 | 30 | def test_temperature_forced(): 31 | from calibration import BMP280Calibration 32 | from tools import SMBusFakeDevice 33 | 34 | from bmp280 import BMP280 35 | dev = SMBusFakeDevice(1) 36 | 37 | # Load the fake temperature into the virtual registers 38 | dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 39 | dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 40 | dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 41 | 42 | bmp280 = BMP280(i2c_dev=dev) 43 | bmp280.setup(mode="forced") 44 | 45 | # Replace the loaded calibration with our known values 46 | bmp280.calibration = BMP280Calibration() 47 | 48 | assert round(bmp280.get_temperature(), 4) == round(TEST_TEMP_CMP, 4) 49 | 50 | 51 | def test_pressure(): 52 | from calibration import BMP280Calibration 53 | from tools import SMBusFakeDevice 54 | 55 | from bmp280 import BMP280 56 | dev = SMBusFakeDevice(1) 57 | 58 | # Load the fake temperature values into the virtual registers 59 | # Pressure is temperature compensated!!! 60 | dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 61 | dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 62 | dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 63 | 64 | # Load the fake pressure values 65 | dev.regs[0xf9] = (TEST_PRES_RAW & 0x0000F) << 4 66 | dev.regs[0xf8] = (TEST_PRES_RAW & 0x00FF0) >> 4 67 | dev.regs[0xf7] = (TEST_PRES_RAW & 0xFF000) >> 12 68 | 69 | bmp280 = BMP280(i2c_dev=dev) 70 | bmp280.setup() 71 | 72 | # Replace the loaded calibration with our known values 73 | bmp280.calibration = BMP280Calibration() 74 | 75 | assert round(bmp280.get_pressure(), 4) == round(TEST_PRES_CMP, 4) 76 | 77 | 78 | def test_altitude(): 79 | from calibration import BMP280Calibration 80 | from tools import SMBusFakeDevice 81 | 82 | from bmp280 import BMP280 83 | dev = SMBusFakeDevice(1) 84 | 85 | # Load the fake temperature values into the virtual registers 86 | # Pressure is temperature compensated!!! 87 | dev.regs[0xfc] = (TEST_TEMP_RAW & 0x0000F) << 4 88 | dev.regs[0xfb] = (TEST_TEMP_RAW & 0x00FF0) >> 4 89 | dev.regs[0xfa] = (TEST_TEMP_RAW & 0xFF000) >> 12 90 | 91 | # Load the fake pressure values 92 | dev.regs[0xf9] = (TEST_PRES_RAW & 0x0000F) << 4 93 | dev.regs[0xf8] = (TEST_PRES_RAW & 0x00FF0) >> 4 94 | dev.regs[0xf7] = (TEST_PRES_RAW & 0xFF000) >> 12 95 | 96 | bmp280 = BMP280(i2c_dev=dev) 97 | bmp280.setup() 98 | 99 | # Replace the loaded calibration with our known values 100 | bmp280.calibration = BMP280Calibration() 101 | 102 | assert round(bmp280.get_altitude(), 4) == round(TEST_ALT_CMP, 4) 103 | -------------------------------------------------------------------------------- /tests/test_setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import mock 4 | import pytest 5 | 6 | 7 | def test_setup_not_present(): 8 | sys.modules['smbus2'] = mock.MagicMock() 9 | from bmp280 import BMP280 10 | bmp280 = BMP280() 11 | with pytest.raises(RuntimeError): 12 | bmp280.setup() 13 | 14 | 15 | def test_setup_mock_present(): 16 | from tools import SMBusFakeDevice 17 | smbus = mock.Mock() 18 | smbus.SMBus = SMBusFakeDevice 19 | sys.modules['smbus2'] = smbus 20 | from bmp280 import BMP280 21 | bmp280 = BMP280() 22 | bmp280.setup() 23 | 24 | 25 | def test_setup_forced_mode(): 26 | from tools import SMBusFakeDevice 27 | smbus = mock.Mock() 28 | smbus.SMBus = SMBusFakeDevice 29 | sys.modules['smbus2'] = smbus 30 | from bmp280 import BMP280 31 | bmp280 = BMP280() 32 | bmp280.setup(mode="forced") 33 | -------------------------------------------------------------------------------- /tests/tools.py: -------------------------------------------------------------------------------- 1 | from i2cdevice import MockSMBus 2 | 3 | 4 | class SMBusFakeDevice(MockSMBus): 5 | def __init__(self, i2c_bus): 6 | MockSMBus.__init__(self, i2c_bus) 7 | self.regs[0xD0] = 0x58 # Fake chip ID 8 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------