├── .github └── workflows │ ├── build.yml │ ├── qa.yml │ └── test.yml ├── .gitignore ├── .stickler.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── check.sh ├── examples ├── all.py └── specific.py ├── install.sh ├── pms5003 └── __init__.py ├── pyproject.toml ├── requirements-dev.txt ├── tests ├── conftest.py └── test_setup.py ├── tox.ini └── uninstall.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Python ${{ matrix.python }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: ['3.9', '3.10', '3.11'] 16 | 17 | env: 18 | RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} 19 | 20 | steps: 21 | - name: Checkout Code 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Python ${{ matrix.python }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python }} 28 | 29 | - name: Install Dependencies 30 | run: | 31 | make dev-deps 32 | 33 | - name: Build Packages 34 | run: | 35 | make build 36 | 37 | - name: Upload Packages 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: ${{ env.RELEASE_FILE }} 41 | path: dist/ 42 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: linting & spelling 12 | runs-on: ubuntu-latest 13 | env: 14 | TERM: xterm-256color 15 | 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python '3,11' 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.11' 24 | 25 | - name: Install Dependencies 26 | run: | 27 | make dev-deps 28 | 29 | - name: Run Quality Assurance 30 | run: | 31 | make qa 32 | 33 | - name: Run Code Checks 34 | run: | 35 | make check 36 | 37 | - name: Run Bash Code Checks 38 | run: | 39 | make shellcheck 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Python ${{ matrix.python }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python: ['3.9', '3.10', '3.11'] 16 | 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Python ${{ matrix.python }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python }} 25 | 26 | - name: Install Dependencies 27 | run: | 28 | make dev-deps 29 | 30 | - name: Run Tests 31 | run: | 32 | make pytest 33 | 34 | - name: Coverage 35 | if: ${{ matrix.python == '3.9' }} 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: | 39 | python -m pip install coveralls 40 | coveralls --service=github 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | _build/ 3 | *.o 4 | *.so 5 | *.a 6 | *.py[cod] 7 | *.egg-info 8 | dist/ 9 | __pycache__ 10 | .DS_Store 11 | *.deb 12 | *.dsc 13 | *.build 14 | *.changes 15 | *.orig.* 16 | packaging/*tar.xz 17 | library/debian/ 18 | .coverage 19 | .pytest_cache 20 | .tox 21 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | flake8: 4 | python: 3 5 | max-line-length: 160 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.1 2 | ----- 3 | 4 | * Remove platform detection and default to Pi-compatible pins 5 | * Support passing in LineRequest and offset for custom pins (supported in gpiodevice>=0.0.4) 6 | 7 | 1.0.0 8 | ----- 9 | 10 | * Repackage to hatch/pyproject.toml 11 | * Port to gpiod/gpiodevice (away from RPi.GPIO) 12 | 13 | 0.0.5 14 | ----- 15 | 16 | * BugFix: Read start-of-frame a byte at a time to avoid misalignment issues, potential fix for #2, #3 and #4 17 | * Enhancement: Clarified error message when length packet cannot be read 18 | * Enhancement: Clarified error message when start of frame cannot be read 19 | * Enhancement: Added new error message where raw data length is less than expected (frame length) 20 | 21 | 0.0.4 22 | ----- 23 | 24 | * Packaging improvements/bugfix from boilerplate 25 | 26 | 0.0.3 27 | ----- 28 | 29 | * Added pyserial dependency 30 | 31 | 0.0.2 32 | ----- 33 | 34 | * Added reset function 35 | 36 | 0.0.1 37 | ----- 38 | 39 | * Initial Release 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) 2 | LIBRARY_VERSION := $(shell hatch version 2> /dev/null) 3 | 4 | .PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy 5 | usage: 6 | ifdef LIBRARY_NAME 7 | @echo "Library: ${LIBRARY_NAME}" 8 | @echo "Version: ${LIBRARY_VERSION}\n" 9 | else 10 | @echo "WARNING: You should 'make dev-deps'\n" 11 | endif 12 | @echo "Usage: make , where target is one of:\n" 13 | @echo "install: install the library locally from source" 14 | @echo "uninstall: uninstall the local library" 15 | @echo "dev-deps: install Python dev dependencies" 16 | @echo "check: perform basic integrity checks on the codebase" 17 | @echo "qa: run linting and package QA" 18 | @echo "pytest: run Python test fixtures" 19 | @echo "clean: clean Python build and dist directories" 20 | @echo "build: build Python distribution files" 21 | @echo "testdeploy: build and upload to test PyPi" 22 | @echo "deploy: build and upload to PyPi" 23 | @echo "tag: tag the repository with the current version\n" 24 | 25 | 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 shellcheck 34 | 35 | check: 36 | @bash check.sh 37 | 38 | shellcheck: 39 | shellcheck *.sh 40 | 41 | qa: 42 | tox -e qa 43 | 44 | pytest: 45 | tox -e py 46 | 47 | nopost: 48 | @bash check.sh --nopost 49 | 50 | tag: 51 | git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" 52 | 53 | build: check 54 | @hatch build 55 | 56 | clean: 57 | -rm -r dist 58 | 59 | testdeploy: build 60 | twine upload --repository testpypi dist/* 61 | 62 | deploy: nopost build 63 | twine upload dist/* 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PMS5003 Particulate Sensor 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/pms5003-python/test.yml?branch=main)](https://github.com/pimoroni/pms5003-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/pms5003-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/pms5003-python?branch=main) 5 | [![PyPi Package](https://img.shields.io/pypi/v/pms5003.svg)](https://pypi.python.org/pypi/pms5003) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/pms5003.svg)](https://pypi.python.org/pypi/pms5003) 7 | 8 | # Installing 9 | 10 | **Note** The code in this repository supports both the Enviro+ and Enviro Mini boards. _The Enviro Mini board does not have the Gas sensor or the breakout for the PM sensor._ 11 | 12 | ![Enviro Plus pHAT](https://raw.githubusercontent.com/pimoroni/enviroplus-python/main/Enviro-Plus-pHAT.jpg) 13 | ![Enviro Mini pHAT](https://raw.githubusercontent.com/pimoroni/enviroplus-python/main/Enviro-mini-pHAT.jpg) 14 | 15 | :warning: This library now supports Python 3 only, Python 2 is EOL - https://www.python.org/doc/sunset-python-2/ 16 | 17 | ## Install and configure dependencies from GitHub: 18 | 19 | * `git clone https://github.com/pimoroni/pms5003-python` 20 | * `cd pms5003-python` 21 | * `./install.sh` 22 | 23 | **Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: 24 | 25 | ``` 26 | source ~/.virtualenvs/pimoroni/bin/activate 27 | ``` 28 | 29 | **Note** Raspbian/Raspberry Pi OS Lite users may first need to install git: `sudo apt install git` 30 | 31 | ## Or... Install from PyPi and configure manually: 32 | 33 | * `python3 -m venv --system-site-packages $HOME/.virtualenvs/pimoroni` 34 | * Run `python3 -m pip install pms5003` 35 | 36 | **Note** this will not perform any of the required configuration changes on your Pi, you may additionally need to: 37 | 38 | ### Bookworm 39 | 40 | * Enable serial: `raspi-config nonint do_serial_hw 0` 41 | * Disable serial terminal: `raspi-config nonint do_serial_cons 1` 42 | * Add `dtoverlay=pi3-miniuart-bt` to your `/boot/config.txt` 43 | 44 | ### Bullseye 45 | 46 | * Enable serial: `raspi-config nonint set_config_var enable_uart 1 /boot/config.txt` 47 | * Disable serial terminal: `sudo raspi-config nonint do_serial 1` 48 | * Add `dtoverlay=pi3-miniuart-bt` to your `/boot/config.txt` 49 | 50 | In both cases the last line will switch Bluetooth over to miniUART, see https://www.raspberrypi.org/documentation/configuration/uart.md for more details. 51 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script handles some basic QA checks on the source 4 | 5 | NOPOST=$1 6 | LIBRARY_NAME=$(hatch project metadata name) 7 | LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') 8 | POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') 9 | TERM=${TERM:="xterm-256color"} 10 | 11 | success() { 12 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 13 | } 14 | 15 | inform() { 16 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 17 | } 18 | 19 | warning() { 20 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 21 | } 22 | 23 | while [[ $# -gt 0 ]]; do 24 | K="$1" 25 | case $K in 26 | -p|--nopost) 27 | NOPOST=true 28 | shift 29 | ;; 30 | *) 31 | if [[ $1 == -* ]]; then 32 | printf "Unrecognised option: %s\n" "$1"; 33 | exit 1 34 | fi 35 | POSITIONAL_ARGS+=("$1") 36 | shift 37 | esac 38 | done 39 | 40 | inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" 41 | 42 | inform "Checking for trailing whitespace..." 43 | if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then 44 | warning "Trailing whitespace found!" 45 | exit 1 46 | else 47 | success "No trailing whitespace found." 48 | fi 49 | printf "\n" 50 | 51 | inform "Checking for DOS line-endings..." 52 | if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then 53 | warning "DOS line-endings found!" 54 | exit 1 55 | else 56 | success "No DOS line-endings found." 57 | fi 58 | printf "\n" 59 | 60 | inform "Checking CHANGELOG.md..." 61 | if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then 62 | warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." 63 | exit 1 64 | else 65 | success "Changes found for version ${LIBRARY_VERSION}." 66 | fi 67 | printf "\n" 68 | 69 | inform "Checking for git tag ${LIBRARY_VERSION}..." 70 | if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then 71 | warning "Missing git tag for version ${LIBRARY_VERSION}" 72 | fi 73 | printf "\n" 74 | 75 | if [[ $NOPOST ]]; then 76 | inform "Checking for .postN on library version..." 77 | if [[ "$POST_VERSION" != "" ]]; then 78 | warning "Found .$POST_VERSION on library version." 79 | inform "Please only use these for testpypi releases." 80 | exit 1 81 | else 82 | success "OK" 83 | fi 84 | fi 85 | -------------------------------------------------------------------------------- /examples/all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from pms5003 import PMS5003 4 | 5 | print( 6 | """all.py - Continuously print all data values. 7 | 8 | Press Ctrl+C to exit! 9 | 10 | """ 11 | ) 12 | 13 | 14 | # Configure the PMS5003 for Enviro+ 15 | # pins and ports may vary for your hardware! 16 | 17 | # Default, assume Raspberry Pi compatible, running Raspberry Pi OS Bookworm 18 | pms5003 = PMS5003(device="/dev/ttyAMA0", baudrate=9600) 19 | 20 | # Raspberry Pi 4 (Raspberry Pi OS) 21 | # 22 | # GPIO22 and GPIO27 are enable and reset for Raspberry Pi 4 23 | # use "raspi-config" to enable serial, or add 24 | # "dtoverlay=uart0" to /boot/config.txt 25 | # 26 | # pms5003 = PMS5003(device="/dev/ttyAMA0", baudrate=9600, pin_enable="GPIO22", pin_reset="GPIO27") 27 | 28 | # Raspberry Pi 5 (Raspberry Pi OS) 29 | # 30 | # GPIO22 and GPIO27 are enable and reset for Raspberry Pi 5 31 | # On older versions of Bookworm these might be PIN15 and PIN13 32 | # use "raspi-config" to enable serial, or add 33 | # "dtoverlay=uart0-pi5" to /boot/firmware/config.txt 34 | # 35 | # pms5003 = PMS5003(device="/dev/ttyAMA0", baudrate=9600, pin_enable="GPIO22", pin_reset="GPIO27") 36 | 37 | # ROCK 5B 38 | # 39 | # Use "armbian-config" to enable rk3568-uart2-m0 40 | # Disable console on ttyS2 with: 41 | # sudo systemctl stop serial-getty@ttyS2.service 42 | # sudo systemctl disable serial-getty@ttyS2.service 43 | # sudo systemctl mask serial-getty@ttyS2.service 44 | # add "console=display" to /boot/armbianEnv.txt 45 | # 46 | # pms5003 = PMS5003(device="/dev/ttyS2", baudrate=9600, pin_enable="PIN_15", pin_reset="PIN_13") 47 | 48 | # Other 49 | # 50 | # Use gpiod to request the pins you want, and pass those into PMS5003 as LineRequest, offset tuples. 51 | # 52 | # from pms5003 import OUTL, OUTH 53 | # from gpiod import Chip 54 | # lines = Chip.request_lines(consumer="PMS5003", config={22: OUTH, 27: OUTL}) 55 | # pms5003 = PMS5003(device="/dev/ttyAMA0", baudrate=9600, pin_enable=(lines, 22), pin_reset=(lines, 27)) 56 | 57 | try: 58 | while True: 59 | data = pms5003.read() 60 | print(data) 61 | 62 | except KeyboardInterrupt: 63 | pass 64 | -------------------------------------------------------------------------------- /examples/specific.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from pms5003 import PMS5003 4 | 5 | print( 6 | """specific.py - Continuously print a specific data value. 7 | 8 | Press Ctrl+C to exit! 9 | 10 | """ 11 | ) 12 | 13 | # Configure the PMS5003 for Enviro+ 14 | # pins and ports may vary for your hardware! 15 | 16 | # Default, assume Raspberry Pi compatible, running Raspberry Pi OS Bookworm 17 | pms5003 = PMS5003(device="/dev/ttyAMA0", baudrate=9600) 18 | 19 | # Raspberry Pi 4 (Raspberry Pi OS) 20 | # 21 | # GPIO22 and GPIO27 are enable and reset for Raspberry Pi 4 22 | # use "raspi-config" to enable serial, or add 23 | # "dtoverlay=uart0" to /boot/config.txt 24 | # 25 | # pms5003 = PMS5003(device="/dev/ttyAMA0", baudrate=9600, pin_enable="GPIO22", pin_reset="GPIO27") 26 | 27 | # Raspberry Pi 5 (Raspberry Pi OS) 28 | # 29 | # GPIO22 and GPIO27 are enable and reset for Raspberry Pi 5 30 | # On older versions of Bookworm these might be PIN15 and PIN13 31 | # use "raspi-config" to enable serial, or add 32 | # "dtoverlay=uart0-pi5" to /boot/firmware/config.txt 33 | # 34 | # pms5003 = PMS5003(device="/dev/ttyAMA0", baudrate=9600, pin_enable="GPIO22", pin_reset="GPIO27") 35 | 36 | # ROCK 5B 37 | # 38 | # Use "armbian-config" to enable rk3568-uart2-m0 39 | # Disable console on ttyS2 with: 40 | # sudo systemctl stop serial-getty@ttyS2.service 41 | # sudo systemctl disable serial-getty@ttyS2.service 42 | # sudo systemctl mask serial-getty@ttyS2.service 43 | # add "console=display" to /boot/armbianEnv.txt 44 | # 45 | # pms5003 = PMS5003(device="/dev/ttyS2", baudrate=9600, pin_enable="PIN_15", pin_reset="PIN_13") 46 | 47 | # Other 48 | # 49 | # Use gpiod to request the pins you want, and pass those into PMS5003 as LineRequest, offset tuples. 50 | # 51 | # from pms5003 import OUTL, OUTH 52 | # from gpiod import Chip 53 | # lines = Chip.request_lines(consumer="PMS5003", config={22: OUTH, 27: OUTL}) 54 | # pms5003 = PMS5003(device="/dev/ttyAMA0", baudrate=9600, pin_enable=(lines, 22), pin_reset=(lines, 27)) 55 | 56 | 57 | try: 58 | while True: 59 | data = pms5003.read() 60 | print(f"PM2.5 ug/m3 (combustion particles, organic compounds, metals): {data.pm_ug_per_m3(2.5)}") 61 | 62 | except KeyboardInterrupt: 63 | pass 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pms5003/__init__.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import time 3 | 4 | import gpiod 5 | import gpiodevice 6 | import serial 7 | from gpiod.line import Direction, Value 8 | 9 | __version__ = "1.0.1" 10 | 11 | 12 | PMS5003_SOF = bytearray(b"\x42\x4d") 13 | 14 | OUTL = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE) 15 | OUTH = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE) 16 | 17 | 18 | class ChecksumMismatchError(RuntimeError): 19 | pass 20 | 21 | 22 | class ReadTimeoutError(RuntimeError): 23 | pass 24 | 25 | 26 | class SerialTimeoutError(RuntimeError): 27 | pass 28 | 29 | 30 | class PMS5003Data: 31 | def __init__(self, raw_data): 32 | self.raw_data = raw_data 33 | self.data = struct.unpack(">HHHHHHHHHHHHHH", raw_data) 34 | self.checksum = self.data[13] 35 | 36 | def pm_ug_per_m3(self, size, atmospheric_environment=False): 37 | if atmospheric_environment: 38 | if size == 1.0: 39 | return self.data[3] 40 | if size == 2.5: 41 | return self.data[4] 42 | if size is None: 43 | return self.data[5] 44 | 45 | else: 46 | if size == 1.0: 47 | return self.data[0] 48 | if size == 2.5: 49 | return self.data[1] 50 | if size == 10: 51 | return self.data[2] 52 | 53 | raise ValueError("Particle size {} measurement not available.".format(size)) 54 | 55 | def pm_per_1l_air(self, size): 56 | if size == 0.3: 57 | return self.data[6] 58 | if size == 0.5: 59 | return self.data[7] 60 | if size == 1.0: 61 | return self.data[8] 62 | if size == 2.5: 63 | return self.data[9] 64 | if size == 5: 65 | return self.data[10] 66 | if size == 10: 67 | return self.data[11] 68 | 69 | raise ValueError("Particle size {} measurement not available.".format(size)) 70 | 71 | def __repr__(self): 72 | return """ 73 | PM1.0 ug/m3 (ultrafine particles): {} 74 | PM2.5 ug/m3 (combustion particles, organic compounds, metals): {} 75 | PM10 ug/m3 (dust, pollen, mould spores): {} 76 | PM1.0 ug/m3 (atmos env): {} 77 | PM2.5 ug/m3 (atmos env): {} 78 | PM10 ug/m3 (atmos env): {} 79 | >0.3um in 0.1L air: {} 80 | >0.5um in 0.1L air: {} 81 | >1.0um in 0.1L air: {} 82 | >2.5um in 0.1L air: {} 83 | >5.0um in 0.1L air: {} 84 | >10um in 0.1L air: {} 85 | """.format( 86 | *self.data[:-2] 87 | ) 88 | 89 | def __str__(self): 90 | return self.__repr__() 91 | 92 | 93 | class PMS5003: 94 | def __init__(self, device="/dev/ttyAMA0", baudrate=9600, pin_enable="GPIO22", pin_reset="GPIO27"): 95 | self._serial = None 96 | self._device = device 97 | self._baudrate = baudrate 98 | 99 | gpiodevice.friendly_errors = True 100 | self._pin_enable = gpiodevice.get_pin(pin_enable, "PMS5003_en", OUTH) 101 | self._pin_reset = gpiodevice.get_pin(pin_reset, "PMS5003_rst", OUTL) 102 | 103 | self.setup() 104 | 105 | def setup(self): 106 | if self._serial is not None: 107 | self._serial.close() 108 | 109 | self._serial = serial.Serial(self._device, baudrate=self._baudrate, timeout=4) 110 | 111 | self.reset() 112 | 113 | def set_pin(self, pin, state): 114 | lines, offset = pin 115 | lines.set_value(offset, Value.ACTIVE if state else Value.INACTIVE) 116 | 117 | def reset(self): 118 | time.sleep(0.1) 119 | self.set_pin(self._pin_reset, False) 120 | self._serial.flushInput() 121 | time.sleep(0.1) 122 | self.set_pin(self._pin_reset, True) 123 | 124 | def read(self): 125 | start = time.time() 126 | 127 | sof_index = 0 128 | 129 | while True: 130 | elapsed = time.time() - start 131 | if elapsed > 5: 132 | raise ReadTimeoutError("PMS5003 Read Timeout: Could not find start of frame") 133 | 134 | sof = self._serial.read(1) 135 | if len(sof) == 0: 136 | raise SerialTimeoutError("PMS5003 Read Timeout: Failed to read start of frame byte") 137 | sof = ord(sof) if isinstance(sof, bytes) else sof 138 | 139 | if sof == PMS5003_SOF[sof_index]: 140 | if sof_index == 0: 141 | sof_index = 1 142 | elif sof_index == 1: 143 | break 144 | else: 145 | sof_index = 0 146 | 147 | checksum = sum(PMS5003_SOF) 148 | 149 | data = bytearray(self._serial.read(2)) # Get frame length packet 150 | if len(data) != 2: 151 | raise SerialTimeoutError("PMS5003 Read Timeout: Could not find length packet") 152 | checksum += sum(data) 153 | frame_length = struct.unpack(">H", data)[0] 154 | 155 | raw_data = bytearray(self._serial.read(frame_length)) 156 | if len(raw_data) != frame_length: 157 | raise SerialTimeoutError("PMS5003 Read Timeout: Invalid frame length. Got {} bytes, expected {}.".format(len(raw_data), frame_length)) 158 | 159 | data = PMS5003Data(raw_data) 160 | # Don't include the checksum bytes in the checksum calculation 161 | checksum += sum(raw_data[:-2]) 162 | 163 | if checksum != data.checksum: 164 | raise ChecksumMismatchError("PMS5003 Checksum Mismatch {} != {}".format(checksum, data.checksum)) 165 | 166 | return data 167 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-fancy-pypi-readme"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pms5003" 7 | dynamic = ["version", "readme"] 8 | description = "PMS5003 Particulate 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 | "gpiod", 39 | "gpiodevice>=0.0.4", 40 | "pyserial" 41 | ] 42 | 43 | [project.urls] 44 | GitHub = "https://www.github.com/pimoroni/pms5003-python" 45 | Homepage = "https://www.pimoroni.com" 46 | 47 | [tool.hatch.version] 48 | path = "pms5003/__init__.py" 49 | 50 | [tool.hatch.build] 51 | include = [ 52 | "pms5003", 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 | 96 | [tool.black] 97 | line-length = 200 98 | 99 | [tool.isort] 100 | line_length = 200 101 | 102 | [tool.check-manifest] 103 | ignore = [ 104 | '.stickler.yml', 105 | 'boilerplate.md', 106 | 'check.sh', 107 | 'install.sh', 108 | 'uninstall.sh', 109 | 'Makefile', 110 | 'tox.ini', 111 | 'tests/*', 112 | 'examples/*', 113 | '.coveragerc', 114 | 'requirements-dev.txt' 115 | ] 116 | 117 | [tool.pimoroni] 118 | apt_packages = [] 119 | configtxt = [] 120 | commands = [ 121 | "printf \"Setting up serial for PMS5003..\\n\"", 122 | "sudo raspi-config nonint do_serial_cons 1", 123 | "sudo raspi-config nonint do_serial_hw 0" 124 | ] 125 | -------------------------------------------------------------------------------- /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/conftest.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import sys 3 | 4 | import mock 5 | import pytest 6 | 7 | 8 | class MockSerial: 9 | def __init__(self, *args, **kwargs): 10 | self.ptr = 0 11 | self.sof = b"\x42\x4d" 12 | self.data = self.sof 13 | self.data += struct.pack(">H", 28) 14 | self.data += b"\x00" * 26 15 | checksum = struct.pack(">H", sum(bytearray(self.data))) 16 | self.data += checksum 17 | 18 | def read(self, length): 19 | result = self.data[self.ptr : self.ptr + length] 20 | self.ptr += length 21 | if self.ptr >= len(self.data): 22 | self.ptr = 0 23 | return result 24 | 25 | def flushInput(self): 26 | pass 27 | 28 | def close(self): 29 | pass 30 | 31 | 32 | class MockSerialFail(MockSerial): 33 | def __init__(self, *args, **kwargs): 34 | pass 35 | 36 | def read(self, length): 37 | return b"\x00" * length 38 | 39 | 40 | @pytest.fixture(scope='function', autouse=False) 41 | def pms5003(): 42 | import pms5003 43 | yield pms5003 44 | del sys.modules['pms5003'] 45 | 46 | 47 | @pytest.fixture(scope='function', autouse=False) 48 | def gpiod(): 49 | sys.modules['gpiod'] = mock.Mock() 50 | sys.modules['gpiod.line'] = mock.Mock() 51 | yield sys.modules['gpiod'] 52 | del sys.modules['gpiod.line'] 53 | del sys.modules['gpiod'] 54 | 55 | 56 | @pytest.fixture(scope='function', autouse=False) 57 | def gpiodevice(): 58 | gpiodevice = mock.Mock() 59 | gpiodevice.get_pins_for_platform.return_value = [(mock.Mock(), 0), (mock.Mock(), 0)] 60 | gpiodevice.get_pin.return_value = (mock.Mock(), 0) 61 | 62 | sys.modules['gpiodevice'] = gpiodevice 63 | yield gpiodevice 64 | del sys.modules['gpiodevice'] 65 | 66 | 67 | @pytest.fixture(scope='function', autouse=False) 68 | def serial(): 69 | sys.modules['serial'] = mock.Mock() 70 | sys.modules['serial'].Serial = MockSerial 71 | yield sys.modules['serial'] 72 | del sys.modules['serial'] 73 | 74 | 75 | @pytest.fixture(scope='function', autouse=False) 76 | def serial_fail(): 77 | sys.modules['serial'] = mock.Mock() 78 | sys.modules['serial'].Serial = MockSerialFail 79 | yield sys.modules['serial'] 80 | del sys.modules['serial'] 81 | 82 | -------------------------------------------------------------------------------- /tests/test_setup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_setup(gpiod, gpiodevice, serial, pms5003): 5 | _ = pms5003.PMS5003() 6 | 7 | 8 | def test_double_setup(gpiod, gpiodevice, serial, pms5003): 9 | sensor = pms5003.PMS5003() 10 | sensor.setup() 11 | 12 | 13 | def test_read(gpiod, gpiodevice, serial, pms5003): 14 | sensor = pms5003.PMS5003() 15 | data = sensor.read() 16 | data.pm_ug_per_m3(2.5) 17 | 18 | 19 | def test_read_fail(gpiod, gpiodevice, serial_fail, pms5003): 20 | sensor = pms5003.PMS5003() 21 | with pytest.raises(pms5003.ReadTimeoutError): 22 | _ = sensor.read() 23 | -------------------------------------------------------------------------------- /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 "%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 | --------------------------------------------------------------------------------