├── .github └── workflows │ └── test.yml ├── .gitignore ├── .stickler.yml ├── LICENSE ├── Makefile ├── README.md ├── REFERENCE.md ├── examples ├── cmd.py ├── latlon.py └── pps.py ├── install-legacy.sh ├── install.sh ├── library ├── .coveragerc ├── CHANGELOG.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── pa1010d │ └── __init__.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tests │ ├── conftest.py │ └── test_setup.py └── tox.ini └── uninstall.sh /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Python Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python: ['3.9', '3.10', '3.11'] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python }} 22 | - name: Install Dependencies 23 | run: | 24 | python -m pip install --upgrade setuptools tox 25 | - name: Run Tests 26 | working-directory: library 27 | run: | 28 | tox -e py 29 | - name: Coverage 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | working-directory: library 33 | run: | 34 | python -m pip install coveralls 35 | coveralls --service=github 36 | if: ${{ matrix.python == '3.11' }} 37 | 38 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}') 2 | LIBRARY_NAME=$(shell grep name library/setup.cfg | awk -F" = " '{print $$2}') 3 | 4 | .PHONY: usage install uninstall 5 | usage: 6 | @echo "Library: ${LIBRARY_NAME}" 7 | @echo "Version: ${LIBRARY_VERSION}\n" 8 | @echo "Usage: make , where target is one of:\n" 9 | @echo "install: install the library locally from source" 10 | @echo "uninstall: uninstall the local library" 11 | @echo "check: peform basic integrity checks on the codebase" 12 | @echo "python-readme: generate library/README.md from README.md + library/CHANGELOG.txt" 13 | @echo "python-wheels: build python .whl files for distribution" 14 | @echo "python-sdist: build python source distribution" 15 | @echo "python-clean: clean python build and dist directories" 16 | @echo "python-dist: build all python distribution files" 17 | @echo "python-testdeploy: build all and deploy to test PyPi" 18 | @echo "tag: tag the repository with the current version" 19 | 20 | install: 21 | ./install.sh 22 | 23 | uninstall: 24 | ./uninstall.sh 25 | 26 | check: 27 | @echo "Checking for trailing whitespace" 28 | @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO 29 | @echo "Checking for DOS line-endings" 30 | @! grep -IUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile 31 | @echo "Checking library/CHANGELOG.txt" 32 | @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} 33 | @echo "Checking library/${LIBRARY_NAME}/__init__.py" 34 | @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" 35 | 36 | tag: 37 | git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" 38 | 39 | python-readme: library/README.md 40 | 41 | python-license: library/LICENSE.txt 42 | 43 | library/README.md: README.md library/CHANGELOG.txt 44 | cp README.md library/README.md 45 | printf "\n# Changelog\n" >> library/README.md 46 | cat library/CHANGELOG.txt >> library/README.md 47 | 48 | library/LICENSE.txt: LICENSE 49 | cp LICENSE library/LICENSE.txt 50 | 51 | python-wheels: python-readme python-license 52 | cd library; python3 setup.py bdist_wheel 53 | cd library; python setup.py bdist_wheel 54 | 55 | python-sdist: python-readme python-license 56 | cd library; python setup.py sdist 57 | 58 | python-clean: 59 | -rm -r library/dist 60 | -rm -r library/build 61 | -rm -r library/*.egg-info 62 | 63 | python-dist: python-clean python-wheels python-sdist 64 | ls library/dist 65 | 66 | python-testdeploy: python-dist 67 | twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* 68 | 69 | python-deploy: check python-dist 70 | twine upload library/dist/* 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pimoroni PA1010D GPS Breakout 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/pa1010d-python/test.yml?branch=master)](https://github.com/pimoroni/pa1010d-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/pa1010d-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/pa1010d-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/pa1010d.svg)](https://pypi.python.org/pypi/pa1010d) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/pa1010d.svg)](https://pypi.python.org/pypi/pa1010d) 7 | 8 | # Pre-requisites 9 | 10 | You must enable: 11 | 12 | * i2c: `sudo raspi-config nonint do_i2c 0` 13 | 14 | You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configuration UI to enable interfaces. 15 | 16 | # Installing 17 | 18 | Stable library and dependencies from GitHub: 19 | 20 | * `git clone https://github.com/pimoroni/pa1010d-python` 21 | * `cd pa1010d-python` 22 | * `sudo ./install.sh` 23 | 24 | Latest/development library and dependencies from GitHub: 25 | 26 | * `git clone https://github.com/pimoroni/pa1010d-python` 27 | * `cd pa1010d-python` 28 | * `sudo ./install.sh --unstable` 29 | 30 | Stable (library only) from PyPi: 31 | 32 | * Just run `python3 -m pip install pa1010d` 33 | -------------------------------------------------------------------------------- /REFERENCE.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | The PA1010D library communicates with the GPS over i2c, reading messages a byte at a time from a single register. 4 | 5 | Messages are decoded using [pynmea2](https://github.com/Knio/pynmea2). 6 | 7 | - [Getting Started](#getting-started) 8 | - [Installing (Stable)](#installing-stable) 9 | - [Installing (Unstable)](#installing-unstable) 10 | - [Function Reference](#function-reference) 11 | - [Update](#update) 12 | - [Timestamp](#timestamp) 13 | - [Latitude & Longitude](#latitude--longitude) 14 | - [Altitude](#altitude) 15 | - [Number of Satellites](#number-of-satellites) 16 | - [GPS Fix Quality](#gps-fix-quality) 17 | - [Speed Over Ground](#speed-over-ground) 18 | 19 | ## Getting Started 20 | 21 | The PA1010D Python library requires Python 3.4 or greater. 22 | 23 | Most people should grab the code from GitHub and run our simple installer. 24 | 25 | ### Installing (Stable) 26 | 27 | ``` 28 | git clone https://github.com/pimoroni/pa1010d-python 29 | cd pa1010d-python 30 | sudo ./install.sh 31 | ``` 32 | 33 | This ensures any dependencies are installed and will copy examples into `~/Pimoroni/pa1010d/` 34 | 35 | You can install just the pa1010d library from pypi by running: 36 | 37 | ``` 38 | sudo pip3 install pa1010d 39 | ``` 40 | 41 | ### Installing (Unstable) 42 | 43 | To install the library and dependencies directly from GitHub you can specify the `--unstable` flag on `./install.sh`: 44 | 45 | ``` 46 | git clone https://github.com/pimoroni/pa1010d-python 47 | cd pa1010d-python 48 | sudo ./install.sh --unstable 49 | ``` 50 | 51 | This will run `setup.py install` in the `library` directory instead of installing the library from pypi. 52 | 53 | ## Function Reference 54 | 55 | In all cases you'll first need to initialise a PA1010D library instance like so: 56 | 57 | ```python 58 | from pa1010d import PA1010D 59 | 60 | gps = PA1010D() 61 | ``` 62 | 63 | ### Update 64 | 65 | The PA1010D GPS is read over i2c via a single register. To guarantee that `update` has actually retrieved new data, the update function will poll for new data until a specific sentence type is decoded or a timeout is reached. 66 | 67 | ```python 68 | gps.update(wait_for="GGA", timeout=5) 69 | ``` 70 | 71 | By default the `update` function will wait 5 seconds for a "GGA" message, decoding and storing the information from any messages in the interim. 72 | 73 | All positioning information is then read from properties. 74 | 75 | ### Timestamp 76 | 77 | ```python 78 | gps.timestamp 79 | ``` 80 | 81 | The timestamp of the last GGA message decoded from the GPS 82 | 83 | ### Latitude & Longitude 84 | 85 | ```python 86 | gps.latitude 87 | gps.longitude 88 | ``` 89 | 90 | The latitude and longitude from the last decoded GGA message. 91 | 92 | ### Altitude 93 | 94 | ```python 95 | gps.altitude 96 | ``` 97 | 98 | The altitude from the last decoded GGA message. 99 | 100 | ### Number of Satellites 101 | 102 | ```python 103 | gps.num_sats 104 | ``` 105 | 106 | The number of satellites from the last decoded GGA message. 107 | 108 | ### GPS Fix Quality 109 | 110 | ```python 111 | gps.gps_qual 112 | ``` 113 | 114 | The fix quality from the last decoded GGA message. 115 | 116 | ### Speed Over Ground 117 | 118 | ```python 119 | gps.speed_over_ground 120 | ``` 121 | 122 | The speed over ground (in knots) from the last decoded RMC message. 123 | -------------------------------------------------------------------------------- /examples/cmd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import time 3 | import sys 4 | 5 | from pa1010d import PA1010D 6 | 7 | 8 | """ 9 | Run raw commands against the PA1010D GPS and return the respones. 10 | 11 | Eg: 12 | PMTK605 = Query Firmware Release Info 13 | PMTK430 = Query Datum 14 | PMTK414 = Query NMEA Output Frequency 15 | PMTK400 = Query Update Rate 16 | PMTK225,<1 or 0> = Enable/Disable PPS 17 | """ 18 | 19 | 20 | def timeout(err=None, timeout=5.0): 21 | if err is None: 22 | err = "Timed out!" 23 | t_start = time.time() 24 | while time.time() - t_start < timeout: 25 | yield 26 | raise TimeoutError(err) 27 | 28 | 29 | responses = { 30 | } 31 | 32 | if len(sys.argv) < 2: 33 | print(f"Usage: {sys.argv[0]} ") 34 | sys.exit() 35 | 36 | command = sys.argv[1] 37 | response = responses.get(command, f"$PMTK{int(command[4:]) + 100}") 38 | 39 | gps = PA1010D() 40 | 41 | gps.update() 42 | 43 | gps.send_command(sys.argv[1]) 44 | 45 | if response: 46 | print(f"Waiting for {response}...") 47 | for t in timeout("Timed out waiting for command response."): 48 | message = gps.read_sentence() 49 | if message.startswith(response): 50 | print(message) 51 | break 52 | -------------------------------------------------------------------------------- /examples/latlon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import time 3 | 4 | from pa1010d import PA1010D 5 | 6 | 7 | gps = PA1010D() 8 | 9 | while True: 10 | result = gps.update() 11 | if result: 12 | print(""" 13 | T: {timestamp} 14 | N: {latitude} 15 | E: {longitude} 16 | Alt: {altitude} 17 | Sats: {num_sats} 18 | Qual: {gps_qual} 19 | Speed: {speed_over_ground} 20 | Fix Type: {mode_fix_type} 21 | PDOP: {pdop} 22 | VDOP: {vdop} 23 | HDOP: {hdop} 24 | """.format(**gps.data)) 25 | time.sleep(1.0) 26 | -------------------------------------------------------------------------------- /examples/pps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | from pa1010d import PA1010D 5 | 6 | 7 | if len(sys.argv) < 2: 8 | print(f"Usage: {sys.argv[0]} ") 9 | sys.exit() 10 | 11 | gps = PA1010D() 12 | 13 | pulse_width = 100 14 | 15 | if len(sys.argv) > 2: 16 | pulse_width = int(sys.argv[2]) 17 | 18 | if sys.argv[1] == "on": 19 | gps.send_command(f"PMTK285,4,{pulse_width}") 20 | else: 21 | gps.send_command(f"PMTK285,0,{pulse_width}") 22 | 23 | result = gps.update() 24 | 25 | print("OK" if result else "Uh oh!") 26 | -------------------------------------------------------------------------------- /install-legacy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 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 | USER_HOME=/home/$SUDO_USER 8 | RESOURCES_TOP_DIR=$USER_HOME/Pimoroni 9 | WD=`pwd` 10 | USAGE="sudo ./install.sh (--unstable)" 11 | POSITIONAL_ARGS=() 12 | FORCE=false 13 | UNSTABLE=false 14 | CODENAME=`lsb_release -sc` 15 | 16 | if [[ $CODENAME == "bullseye" ]]; then 17 | bash ./install.sh $@ 18 | exit $? 19 | fi 20 | 21 | user_check() { 22 | if [ $(id -u) -ne 0 ]; then 23 | printf "Script must be run as root. Try 'sudo ./install.sh'\n" 24 | exit 1 25 | fi 26 | } 27 | 28 | confirm() { 29 | if $FORCE; then 30 | true 31 | else 32 | read -r -p "$1 [y/N] " response < /dev/tty 33 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 34 | true 35 | else 36 | false 37 | fi 38 | fi 39 | } 40 | 41 | prompt() { 42 | read -r -p "$1 [y/N] " response < /dev/tty 43 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 44 | true 45 | else 46 | false 47 | fi 48 | } 49 | 50 | success() { 51 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 52 | } 53 | 54 | inform() { 55 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 56 | } 57 | 58 | warning() { 59 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 60 | } 61 | 62 | function do_config_backup { 63 | if [ ! $CONFIG_BACKUP == true ]; then 64 | CONFIG_BACKUP=true 65 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 66 | inform "Backing up $CONFIG to /boot/$FILENAME\n" 67 | cp $CONFIG /boot/$FILENAME 68 | mkdir -p $RESOURCES_TOP_DIR/config-backups/ 69 | cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME 70 | if [ -f "$UNINSTALLER" ]; then 71 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER 72 | fi 73 | fi 74 | } 75 | 76 | function apt_pkg_install { 77 | PACKAGES=() 78 | PACKAGES_IN=("$@") 79 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 80 | PACKAGE="${PACKAGES_IN[$i]}" 81 | if [ "$PACKAGE" == "" ]; then continue; fi 82 | printf "Checking for $PACKAGE\n" 83 | dpkg -L $PACKAGE > /dev/null 2>&1 84 | if [ "$?" == "1" ]; then 85 | PACKAGES+=("$PACKAGE") 86 | fi 87 | done 88 | PACKAGES="${PACKAGES[@]}" 89 | if ! [ "$PACKAGES" == "" ]; then 90 | echo "Installing missing packages: $PACKAGES" 91 | if [ ! $APT_HAS_UPDATED ]; then 92 | apt update 93 | APT_HAS_UPDATED=true 94 | fi 95 | apt install -y $PACKAGES 96 | if [ -f "$UNINSTALLER" ]; then 97 | echo "apt uninstall -y $PACKAGES" 98 | fi 99 | fi 100 | } 101 | 102 | while [[ $# -gt 0 ]]; do 103 | K="$1" 104 | case $K in 105 | -u|--unstable) 106 | UNSTABLE=true 107 | shift 108 | ;; 109 | -f|--force) 110 | FORCE=true 111 | shift 112 | ;; 113 | *) 114 | if [[ $1 == -* ]]; then 115 | printf "Unrecognised option: $1\n"; 116 | printf "Usage: $USAGE\n"; 117 | exit 1 118 | fi 119 | POSITIONAL_ARGS+=("$1") 120 | shift 121 | esac 122 | done 123 | 124 | user_check 125 | 126 | apt_pkg_install python-configparser 127 | 128 | CONFIG_VARS=`python - < $UNINSTALLER 165 | printf "It's recommended you run these steps manually.\n" 166 | printf "If you want to run the full script, open it in\n" 167 | printf "an editor and remove 'exit 1' from below.\n" 168 | exit 1 169 | EOF 170 | 171 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" 172 | 173 | if $UNSTABLE; then 174 | warning "Installing unstable library from source.\n\n" 175 | else 176 | printf "Installing stable library from pypi.\n\n" 177 | fi 178 | 179 | cd library 180 | 181 | printf "Installing for Python 2..\n" 182 | apt_pkg_install "${PY2_DEPS[@]}" 183 | if $UNSTABLE; then 184 | python setup.py install > /dev/null 185 | else 186 | pip install --upgrade $LIBRARY_NAME 187 | fi 188 | if [ $? -eq 0 ]; then 189 | success "Done!\n" 190 | echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER 191 | fi 192 | 193 | if [ -f "/usr/bin/python3" ]; then 194 | printf "Installing for Python 3..\n" 195 | apt_pkg_install "${PY3_DEPS[@]}" 196 | if $UNSTABLE; then 197 | python3 setup.py install > /dev/null 198 | else 199 | pip3 install --upgrade $LIBRARY_NAME 200 | fi 201 | if [ $? -eq 0 ]; then 202 | success "Done!\n" 203 | echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER 204 | fi 205 | fi 206 | 207 | cd $WD 208 | 209 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 210 | CMD="${SETUP_CMDS[$i]}" 211 | # Attempt to catch anything that touches /boot/config.txt and trigger a backup 212 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then 213 | do_config_backup 214 | fi 215 | eval $CMD 216 | done 217 | 218 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 219 | CONFIG_LINE="${CONFIG_TXT[$i]}" 220 | if ! [ "$CONFIG_LINE" == "" ]; then 221 | do_config_backup 222 | inform "Adding $CONFIG_LINE to $CONFIG\n" 223 | sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG 224 | if ! grep -q "^$CONFIG_LINE" $CONFIG; then 225 | printf "$CONFIG_LINE\n" >> $CONFIG 226 | fi 227 | fi 228 | done 229 | 230 | if [ -d "examples" ]; then 231 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 232 | inform "Copying examples to $RESOURCES_DIR" 233 | cp -r examples/ $RESOURCES_DIR 234 | echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER 235 | success "Done!" 236 | fi 237 | fi 238 | 239 | printf "\n" 240 | 241 | if [ -f "/usr/bin/pydoc" ]; then 242 | printf "Generating documentation.\n" 243 | pydoc -w $LIBRARY_NAME > /dev/null 244 | if [ -f "$LIBRARY_NAME.html" ]; then 245 | cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html 246 | rm -f $LIBRARY_NAME.html 247 | inform "Documentation saved to $RESOURCES_DIR/docs.html" 248 | success "Done!" 249 | else 250 | warning "Error: Failed to generate documentation." 251 | fi 252 | fi 253 | 254 | success "\nAll done!" 255 | inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" 256 | inform "Find uninstall steps in $UNINSTALLER\n" 257 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CONFIG=/boot/config.txt 3 | DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` 4 | CONFIG_BACKUP=false 5 | APT_HAS_UPDATED=false 6 | USER_HOME=/home/$SUDO_USER 7 | RESOURCES_TOP_DIR=$USER_HOME/Pimoroni 8 | WD=`pwd` 9 | USAGE="sudo ./install.sh (--unstable)" 10 | POSITIONAL_ARGS=() 11 | FORCE=false 12 | UNSTABLE=false 13 | PYTHON="/usr/bin/python3" 14 | 15 | 16 | user_check() { 17 | if [ $(id -u) -ne 0 ]; then 18 | printf "Script must be run as root. Try 'sudo ./install.sh'\n" 19 | exit 1 20 | fi 21 | } 22 | 23 | confirm() { 24 | if $FORCE; then 25 | true 26 | else 27 | read -r -p "$1 [y/N] " response < /dev/tty 28 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 29 | true 30 | else 31 | false 32 | fi 33 | fi 34 | } 35 | 36 | prompt() { 37 | read -r -p "$1 [y/N] " response < /dev/tty 38 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 39 | true 40 | else 41 | false 42 | fi 43 | } 44 | 45 | success() { 46 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 47 | } 48 | 49 | inform() { 50 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 51 | } 52 | 53 | warning() { 54 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 55 | } 56 | 57 | function do_config_backup { 58 | if [ ! $CONFIG_BACKUP == true ]; then 59 | CONFIG_BACKUP=true 60 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 61 | inform "Backing up $CONFIG to /boot/$FILENAME\n" 62 | cp $CONFIG /boot/$FILENAME 63 | mkdir -p $RESOURCES_TOP_DIR/config-backups/ 64 | cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME 65 | if [ -f "$UNINSTALLER" ]; then 66 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER 67 | fi 68 | fi 69 | } 70 | 71 | function apt_pkg_install { 72 | PACKAGES=() 73 | PACKAGES_IN=("$@") 74 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 75 | PACKAGE="${PACKAGES_IN[$i]}" 76 | if [ "$PACKAGE" == "" ]; then continue; fi 77 | printf "Checking for $PACKAGE\n" 78 | dpkg -L $PACKAGE > /dev/null 2>&1 79 | if [ "$?" == "1" ]; then 80 | PACKAGES+=("$PACKAGE") 81 | fi 82 | done 83 | PACKAGES="${PACKAGES[@]}" 84 | if ! [ "$PACKAGES" == "" ]; then 85 | echo "Installing missing packages: $PACKAGES" 86 | if [ ! $APT_HAS_UPDATED ]; then 87 | apt update 88 | APT_HAS_UPDATED=true 89 | fi 90 | apt install -y $PACKAGES 91 | if [ -f "$UNINSTALLER" ]; then 92 | echo "apt uninstall -y $PACKAGES" 93 | fi 94 | fi 95 | } 96 | 97 | while [[ $# -gt 0 ]]; do 98 | K="$1" 99 | case $K in 100 | -u|--unstable) 101 | UNSTABLE=true 102 | shift 103 | ;; 104 | -f|--force) 105 | FORCE=true 106 | shift 107 | ;; 108 | -p|--python) 109 | PYTHON=$2 110 | shift 111 | shift 112 | ;; 113 | *) 114 | if [[ $1 == -* ]]; then 115 | printf "Unrecognised option: $1\n"; 116 | printf "Usage: $USAGE\n"; 117 | exit 1 118 | fi 119 | POSITIONAL_ARGS+=("$1") 120 | shift 121 | esac 122 | done 123 | 124 | user_check 125 | 126 | if [ ! -f "$PYTHON" ]; then 127 | printf "Python path $PYTHON not found!\n" 128 | exit 1 129 | fi 130 | 131 | PYTHON_VER=`$PYTHON --version` 132 | 133 | inform "Installing. Please wait..." 134 | 135 | $PYTHON -m pip install --upgrade configparser 136 | 137 | CONFIG_VARS=`$PYTHON - < $UNINSTALLER 174 | printf "It's recommended you run these steps manually.\n" 175 | printf "If you want to run the full script, open it in\n" 176 | printf "an editor and remove 'exit 1' from below.\n" 177 | exit 1 178 | EOF 179 | 180 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" 181 | 182 | if $UNSTABLE; then 183 | warning "Installing unstable library from source.\n\n" 184 | else 185 | printf "Installing stable library from pypi.\n\n" 186 | fi 187 | 188 | cd library 189 | 190 | printf "Installing for $PYTHON_VER...\n" 191 | apt_pkg_install "${PY3_DEPS[@]}" 192 | if $UNSTABLE; then 193 | $PYTHON setup.py install > /dev/null 194 | else 195 | $PYTHON -m pip install --upgrade $LIBRARY_NAME 196 | fi 197 | if [ $? -eq 0 ]; then 198 | success "Done!\n" 199 | echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER 200 | fi 201 | 202 | cd $WD 203 | 204 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 205 | CMD="${SETUP_CMDS[$i]}" 206 | # Attempt to catch anything that touches /boot/config.txt and trigger a backup 207 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then 208 | do_config_backup 209 | fi 210 | eval $CMD 211 | done 212 | 213 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 214 | CONFIG_LINE="${CONFIG_TXT[$i]}" 215 | if ! [ "$CONFIG_LINE" == "" ]; then 216 | do_config_backup 217 | inform "Adding $CONFIG_LINE to $CONFIG\n" 218 | sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG 219 | if ! grep -q "^$CONFIG_LINE" $CONFIG; then 220 | printf "$CONFIG_LINE\n" >> $CONFIG 221 | fi 222 | fi 223 | done 224 | 225 | if [ -d "examples" ]; then 226 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 227 | inform "Copying examples to $RESOURCES_DIR" 228 | cp -r examples/ $RESOURCES_DIR 229 | echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER 230 | success "Done!" 231 | fi 232 | fi 233 | 234 | printf "\n" 235 | 236 | if [ -f "/usr/bin/pydoc" ]; then 237 | printf "Generating documentation.\n" 238 | pydoc -w $LIBRARY_NAME > /dev/null 239 | if [ -f "$LIBRARY_NAME.html" ]; then 240 | cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html 241 | rm -f $LIBRARY_NAME.html 242 | inform "Documentation saved to $RESOURCES_DIR/docs.html" 243 | success "Done!" 244 | else 245 | warning "Error: Failed to generate documentation." 246 | fi 247 | fi 248 | 249 | success "\nAll done!" 250 | inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" 251 | inform "Find uninstall steps in $UNINSTALLER\n" 252 | -------------------------------------------------------------------------------- /library/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = pa1010d 3 | omit = 4 | .tox/* 5 | -------------------------------------------------------------------------------- /library/CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.0.4 2 | ----- 3 | 4 | * Fix read/write for Python 3.9 5 | 6 | 0.0.3 7 | ----- 8 | 9 | * Add support for geoidal separation 10 | * Add support for lat/long direction 11 | * Require Python >= 3.6 12 | 13 | 0.0.2 14 | ----- 15 | 16 | * Add support for disabling PPS LED 17 | * Handle spurious newlines in sentences 18 | * Add PDOP, VDOP and HDOP output 19 | * Handle GLL messages 20 | 21 | 0.0.1 22 | ----- 23 | 24 | * Initial Release 25 | -------------------------------------------------------------------------------- /library/LICENSE.txt: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /library/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.txt 2 | include LICENSE.txt 3 | include README.md 4 | include setup.py 5 | recursive-include pa1010d *.py 6 | -------------------------------------------------------------------------------- /library/README.md: -------------------------------------------------------------------------------- 1 | # Pimoroni PA1010D GPS Breakout 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/pa1010d-python/test.yml?branch=master)](https://github.com/pimoroni/pa1010d-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/pa1010d-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/pa1010d-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/pa1010d.svg)](https://pypi.python.org/pypi/pa1010d) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/pa1010d.svg)](https://pypi.python.org/pypi/pa1010d) 7 | 8 | # Pre-requisites 9 | 10 | You must enable: 11 | 12 | * i2c: `sudo raspi-config nonint do_i2c 0` 13 | 14 | You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configuration UI to enable interfaces. 15 | 16 | # Installing 17 | 18 | Stable library and dependencies from GitHub: 19 | 20 | * `git clone https://github.com/pimoroni/pa1010d-python` 21 | * `cd pa1010d-python` 22 | * `sudo ./install.sh` 23 | 24 | Latest/development library and dependencies from GitHub: 25 | 26 | * `git clone https://github.com/pimoroni/pa1010d-python` 27 | * `cd pa1010d-python` 28 | * `sudo ./install.sh --unstable` 29 | 30 | Stable (library only) from PyPi: 31 | 32 | * Just run `python3 -m pip install pa1010d` 33 | 34 | # Changelog 35 | 36 | 0.0.4 37 | ----- 38 | 39 | * Fix read/write for Python 3.9 40 | 41 | 0.0.3 42 | ----- 43 | 44 | * Add support for geoidal separation 45 | * Add support for lat/long direction 46 | * Require Python >= 3.6 47 | 48 | 0.0.2 49 | ----- 50 | 51 | * Add support for disabling PPS LED 52 | * Handle spurious newlines in sentences 53 | * Add PDOP, VDOP and HDOP output 54 | * Handle GLL messages 55 | 56 | 0.0.1 57 | ----- 58 | 59 | * Initial Release 60 | -------------------------------------------------------------------------------- /library/pa1010d/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | import smbus 3 | 4 | 5 | import pynmea2 6 | 7 | 8 | __version__ = '0.0.4' 9 | 10 | PA1010D_ADDR = 0x10 11 | 12 | PPS_DISABLE = 0 13 | PPS_AFTER_FIRST_FIX = 1 14 | PPS_3D_FIX_ONLY = 2 15 | PPS_3D_2D_FIX_ONLY = 3 16 | PPS_ALWAYS = 4 17 | 18 | 19 | class PA1010D(): 20 | __slots__ = ( 21 | "timestamp", 22 | "latitude", 23 | "longitude", 24 | "altitude", 25 | "lat_dir", 26 | "lon_dir", 27 | "geo_sep", 28 | "num_sats", 29 | "gps_qual", 30 | "speed_over_ground", 31 | "mode_fix_type", 32 | "pdop", 33 | "hdop", 34 | "vdop", 35 | "_i2c_addr", 36 | "_i2c", 37 | "_debug" 38 | ) 39 | 40 | def __init__(self, i2c_addr=PA1010D_ADDR, debug=False): 41 | self._i2c_addr = i2c_addr 42 | self._i2c = smbus.SMBus(1) 43 | 44 | self._debug = debug 45 | 46 | self.timestamp = None 47 | self.latitude = None 48 | self.longitude = None 49 | self.altitude = None 50 | self.num_sats = None 51 | self.gps_qual = None 52 | 53 | self.lat_dir = None 54 | self.lon_dir = None 55 | self.geo_sep = None 56 | 57 | self.pdop = None 58 | self.hdop = None 59 | self.vdop = None 60 | 61 | self.speed_over_ground = None 62 | self.mode_fix_type = None 63 | 64 | @property 65 | def data(self): 66 | return dict((slot, getattr(self, slot)) for slot in self.__slots__) 67 | 68 | def _write_sentence(self, bytestring): 69 | """Write a sentence to the PA1010D device over i2c. 70 | 71 | We could- in theory- do this in one burst, but since smbus is limited to 32bytes, 72 | we would have to chunk the message and this is already one byte chunks anyway! 73 | 74 | """ 75 | for char_index in bytestring: 76 | self._i2c.write_byte(self._i2c_addr, char_index) 77 | 78 | def send_command(self, command, add_checksum=True): 79 | """Send a command string to the PA1010D. 80 | 81 | If add_checksum is True (the default) a NMEA checksum will automatically be computed and added. 82 | 83 | """ 84 | if type(command) is not bytes: 85 | command = command.encode("ascii") 86 | 87 | # TODO replace with pynmea2 functionality 88 | if command[0] == b"$": 89 | command = command[1:] 90 | if command[-1] == b"*": 91 | command = command[:-1] 92 | 93 | buf = bytearray() 94 | buf += b'$' 95 | buf += command 96 | if add_checksum: 97 | checksum = 0 98 | # bytes() is a real thing in Python 3 99 | # so `for char in commaud` iterates through char ordinals 100 | for char in command: 101 | checksum ^= char 102 | buf += b'*' # Delimits checksum value 103 | buf += "{checksum:02X}".format(checksum=checksum).encode("ascii") 104 | buf += b'\r\n' 105 | self._write_sentence(buf) 106 | 107 | def read_sentence(self, timeout=5): 108 | """Attempt to read an NMEA sentence from the PA1010D.""" 109 | buf = [] 110 | timeout += time.time() 111 | 112 | while time.time() < timeout: 113 | char = self._i2c.read_byte_data(self._i2c_addr, 0x00) 114 | 115 | if len(buf) == 0 and char != ord("$"): 116 | continue 117 | 118 | buf += [char] 119 | 120 | # Check for end of line 121 | # Should be a full \r\n since the GPS emits spurious newlines 122 | if buf[-2:] == [ord("\r"), ord("\n")]: 123 | # Remove line ending and spurious newlines from the sentence 124 | return bytearray(buf).decode("ascii").strip().replace("\n", "") 125 | 126 | raise TimeoutError("Timeout waiting for readline") 127 | 128 | def update(self, wait_for="GGA", timeout=5): 129 | """Attempt to update from PA1010D. 130 | 131 | Returns true if a sentence has been successfully parsed. 132 | 133 | Returns false if an error has occured. 134 | 135 | Will wait 5 seconds for a GGA message by default. 136 | 137 | :param wait_for: Message type to wait for. 138 | :param timeout: Wait timeout in seconds 139 | 140 | """ 141 | timeout += time.time() 142 | 143 | while time.time() < timeout: 144 | try: 145 | sentence = self.read_sentence() 146 | except TimeoutError: 147 | continue 148 | 149 | try: 150 | result = pynmea2.parse(sentence) 151 | except pynmea2.nmea.ParseError: 152 | if self._debug: 153 | print("Parse error: {sentence}".format(sentence=sentence)) 154 | continue 155 | 156 | # Time, position and fix 157 | if type(result) == pynmea2.GGA: 158 | if result.gps_qual is None: 159 | self.num_sats = 0 160 | self.gps_qual = 0 161 | else: 162 | self.timestamp = result.timestamp 163 | self.latitude = result.latitude 164 | self.longitude = result.longitude 165 | self.lat_dir = result.lat_dir 166 | self.lon_dir = result.lon_dir 167 | self.altitude = result.altitude 168 | self.geo_sep = result.geo_sep 169 | self.num_sats = result.num_sats 170 | self.gps_qual = result.gps_qual 171 | if wait_for == "GGA": 172 | return True 173 | 174 | # Geographic Lat/Lon (Loran holdover) 175 | elif type(result) == pynmea2.GLL: 176 | pass 177 | 178 | # GPS DOP and active satellites 179 | elif type(result) == pynmea2.GSA: 180 | self.mode_fix_type = result.mode_fix_type 181 | self.pdop = result.pdop 182 | self.hdop = result.hdop 183 | self.vdop = result.vdop 184 | if wait_for == "GSA": 185 | return True 186 | 187 | # Position, velocity and time 188 | elif type(result) == pynmea2.RMC: 189 | self.speed_over_ground = result.spd_over_grnd 190 | if wait_for == "RMC": 191 | return True 192 | 193 | # Track made good and speed over ground 194 | elif type(result) == pynmea2.VTG: 195 | if wait_for == "VTG": 196 | return True 197 | 198 | # SVs in view, PRN, elevation, azimuth and SNR 199 | elif type(result) == pynmea2.GSV: 200 | if wait_for == "GSV": 201 | return True 202 | 203 | # ProprietarySentence handles boot up output such as "$PMTK011,MTKGPS*08" 204 | elif type(result) == pynmea2.ProprietarySentence: 205 | # TODO If we implement sending commands *to* the GPS, 206 | # they should not be permitted until after receiving this sequence 207 | # $PMTK011,MTKGPS*08 Successful bootup 208 | # $PMTK010,001*2E Startup 209 | # $PMTK010,002*2D Wake from standby, normal operation 210 | print(sentence) 211 | return True 212 | 213 | else: 214 | # If native MTK support exists, check for those message types 215 | # requires merge and release of: https://github.com/Knio/pynmea2/pull/111 216 | # TODO Drop this special case when #111 is merged & released 217 | try: 218 | if type(result) in ( 219 | pynmea2.types.proprietary.mtk.MTK011, 220 | pynmea2.types.proprietary.mtk.MTK010 221 | ): 222 | return True 223 | except AttributeError: 224 | pass 225 | raise RuntimeError("Unsupported message type {type} ({sentence})".format(type=type(result), sentence=sentence)) 226 | 227 | raise TimeoutError("Timeout waiting for {wait_for} message.".format(wait_for=wait_for)) 228 | 229 | def set_pps(self, mode, pulse_width=100): 230 | if mode not in (0, 1, 2, 3, 4): 231 | raise ValueError("Invalid PPS mode (0 to 4)") 232 | 233 | if pulse_width > 900 or pulse_width < 1: 234 | raise ValueError("Invalid PPS pulse_width (1 to 900ms)") 235 | 236 | self.send_command(f"PMTK285,{mode},{pulse_width}") 237 | 238 | 239 | if __name__ == "__main__": 240 | gps = PA1010D() 241 | 242 | while True: 243 | result = gps.update() 244 | if result: 245 | print(f""" 246 | Time: {gps.timestamp} 247 | Longitude: {gps.longitude: .5f} {gps.lon_dir} 248 | Latitude: {gps.latitude: .5f} {gps.lat_dir} 249 | Altitude: {gps.altitude} 250 | Geoid_Sep: {gps.geo_sep} 251 | Geoid_Alt: {float(gps.altitude) + -float(gps.geo_sep)} 252 | Used Sats: {gps.num_sats} 253 | Quality: {gps.gps_qual}""") 254 | time.sleep(1.0) 255 | -------------------------------------------------------------------------------- /library/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=40.8.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /library/setup.cfg: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | [metadata] 3 | name = pa1010d 4 | version = 0.0.4 5 | author = Philip Howard 6 | author_email = phil@pimoroni.com 7 | description = Python library for the PA1010D i2c GPS module 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | keywords = Raspberry Pi 11 | url = https://www.pimoroni.com 12 | project_urls = 13 | GitHub=https://www.github.com/pimoroni/pa1010d-python 14 | license = MIT 15 | # This includes the license file(s) in the wheel. 16 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 17 | license_files = LICENSE.txt 18 | classifiers = 19 | Development Status :: 4 - Beta 20 | Operating System :: POSIX :: Linux 21 | License :: OSI Approved :: MIT License 22 | Intended Audience :: Developers 23 | Programming Language :: Python :: 3 24 | Topic :: Software Development 25 | Topic :: Software Development :: Libraries 26 | Topic :: System :: Hardware 27 | 28 | [options] 29 | python_requires = >= 3.6 30 | packages = pa1010d 31 | install_requires = pynmea2 32 | 33 | [flake8] 34 | exclude = 35 | .tox, 36 | .eggs, 37 | .git, 38 | __pycache__, 39 | build, 40 | dist 41 | ignore = 42 | E501 43 | 44 | [pimoroni] 45 | py3only = true 46 | py2deps = 47 | py3deps = 48 | configtxt = 49 | commands = 50 | printf "Setting up i2c...\n" 51 | raspi-config nonint do_i2c 0 52 | -------------------------------------------------------------------------------- /library/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Copyright (c) 2016 Pimoroni 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from setuptools import setup, __version__ 26 | from pkg_resources import parse_version 27 | 28 | minimum_version = parse_version('30.4.0') 29 | 30 | if parse_version(__version__) < minimum_version: 31 | raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version)) 32 | 33 | setup() 34 | -------------------------------------------------------------------------------- /library/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import mock 3 | import sys 4 | 5 | 6 | class SMBus: 7 | def __init__(self, bus): 8 | self.data = "$PMTK011,MTKGPS*08\r\n".encode("ascii") 9 | self.ptr = 0 10 | 11 | def read_byte_data(self, address, register): 12 | if register == 0x00: 13 | result = self.data[self.ptr] 14 | self.ptr += 1 15 | self.ptr %= len(self.data) 16 | return result 17 | else: 18 | return 0 19 | 20 | def write_byte(self, address, data): 21 | pass 22 | 23 | 24 | @pytest.fixture(scope='function', autouse=False) 25 | def smbus(): 26 | smbus = mock.MagicMock() 27 | smbus.SMBus = SMBus 28 | sys.modules["smbus"] = smbus 29 | yield smbus 30 | del sys.modules["smbus"] 31 | -------------------------------------------------------------------------------- /library/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | def test_setup(smbus): 2 | import pa1010d 3 | 4 | gps = pa1010d.PA1010D() 5 | del gps 6 | 7 | 8 | def test_send_command(smbus): 9 | import pa1010d 10 | 11 | gps = pa1010d.PA1010D() 12 | gps.send_command("$TEST") 13 | gps.send_command("$TEST*") 14 | gps.send_command("$TEST*".encode("ascii")) 15 | 16 | 17 | def test_recv_command(smbus): 18 | import pa1010d 19 | 20 | gps = pa1010d.PA1010D() 21 | assert gps.update() is True 22 | -------------------------------------------------------------------------------- /library/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py,qa 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | commands = 7 | python setup.py install 8 | coverage run -m pytest -v -r wsx 9 | coverage report 10 | deps = 11 | mock 12 | pytest>=3.1 13 | pytest-cov 14 | 15 | [testenv:qa] 16 | commands = 17 | check-manifest --ignore tox.ini,tests/*,.coveragerc 18 | python setup.py sdist bdist_wheel 19 | twine check dist/* 20 | flake8 --ignore E501 21 | deps = 22 | check-manifest 23 | flake8 24 | twine 25 | 26 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LIBRARY_VERSION=`cat library/setup.cfg | grep version | awk -F" = " '{print $2}'` 4 | LIBRARY_NAME=`cat library/setup.cfg | grep name | awk -F" = " '{print $2}'` 5 | 6 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n" 7 | 8 | if [ $(id -u) -ne 0 ]; then 9 | printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" 10 | exit 1 11 | fi 12 | 13 | cd library 14 | 15 | printf "Unnstalling for Python 2..\n" 16 | pip uninstall $LIBRARY_NAME 17 | 18 | if [ -f "/usr/bin/pip3" ]; then 19 | printf "Uninstalling for Python 3..\n" 20 | pip3 uninstall $LIBRARY_NAME 21 | fi 22 | 23 | cd .. 24 | 25 | printf "Done!\n" 26 | --------------------------------------------------------------------------------