├── MANIFEST.in ├── pizero_gpslog ├── DejaVuSansMono.ttf ├── DejaVuSansCondensed.ttf ├── tests │ ├── data │ │ ├── gpsd │ │ │ ├── bu303-nofix.log │ │ │ ├── bu303-climbing.log │ │ │ ├── bu303-moving.log │ │ │ ├── bu303-stillfix.log │ │ │ ├── README.rst │ │ │ ├── bu303-nofix.log.chk │ │ │ ├── COPYING │ │ │ ├── bu353s4.log │ │ │ ├── bu303-climbing.log.chk │ │ │ ├── bu303-stillfix.log.chk │ │ │ ├── bu303-moving.log.chk │ │ │ └── bu353s4.log.chk │ │ └── runfake.sh │ ├── __init__.py │ └── test_version.py ├── __init__.py ├── displays │ ├── __init__.py │ ├── base.py │ ├── dummy.py │ ├── adafruit4567.py │ └── epd2in13bc.py ├── extradata │ ├── __init__.py │ ├── base.py │ ├── dummy.py │ └── gq_gmc500plus.py ├── utils.py ├── version.py ├── fakeled.py ├── screentest.py ├── displaymanager.py ├── installer.py ├── runner.py ├── converter.py └── gpsd.py ├── setup.cfg ├── .coveragerc ├── tox.ini ├── .travis.yml ├── .gitignore ├── CHANGES.rst ├── setup.py ├── setup_pi.sh └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst 2 | include LICENSE 3 | include README.rst 4 | include pizero_gpslog/*.ttf 5 | -------------------------------------------------------------------------------- /pizero_gpslog/DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jantman/pizero-gpslog/HEAD/pizero_gpslog/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /pizero_gpslog/DejaVuSansCondensed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jantman/pizero-gpslog/HEAD/pizero_gpslog/DejaVuSansCondensed.ttf -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu303-nofix.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jantman/pizero-gpslog/HEAD/pizero_gpslog/tests/data/gpsd/bu303-nofix.log -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu303-climbing.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jantman/pizero-gpslog/HEAD/pizero_gpslog/tests/data/gpsd/bu303-climbing.log -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu303-moving.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jantman/pizero-gpslog/HEAD/pizero_gpslog/tests/data/gpsd/bu303-moving.log -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu303-stillfix.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jantman/pizero-gpslog/HEAD/pizero_gpslog/tests/data/gpsd/bu303-stillfix.log -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [pycodestyle] 5 | exclude = lib/*,lib64/*,pizero_gpslog/installer.py,pizero_gpslog/displays/epd2in13bc.py 6 | max-line-length = 90 7 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/README.rst: -------------------------------------------------------------------------------- 1 | pizero_gpslog/tests/data/gpsd 2 | ============================= 3 | 4 | The log files in this directory came from the ``test/`` directory of the gpsd source code. 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = lib/* 4 | pizero-gpslog/tests/* 5 | setup.py 6 | 7 | [report] 8 | exclude_lines = 9 | # this cant ever be run by py.test, but it just calls one function, 10 | # so ignore it 11 | if __name__ == .__main__.: 12 | if sys.version_info.+ 13 | raise NotImplementedError 14 | except ImportError: 15 | .*# nocoverage.* 16 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/runfake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | FTYPE=moving 5 | 6 | while [[ $# -gt 0 ]]; do 7 | key="$1" 8 | 9 | case $key in 10 | --nofix) 11 | FTYPE=nofix 12 | shift # past argument 13 | ;; 14 | --stillfix) 15 | FTYPE=stillfix 16 | shift # past argument 17 | ;; 18 | -h|--help) 19 | echo "USAGE: runfake.sh [--nofix|--stillfix]" 20 | exit 1 21 | ;; 22 | esac 23 | done 24 | 25 | gpsfake -S ${DIR}/gpsd/bu303-${FTYPE}.log 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38 3 | 4 | [testenv] 5 | deps = 6 | cov-core 7 | coverage 8 | execnet 9 | pycodestyle 10 | py 11 | pytest 12 | pytest-cache 13 | pytest-cov 14 | pytest-pycodestyle 15 | pytest-flakes 16 | mock 17 | freezegun 18 | pytest-blockage 19 | 20 | passenv=TRAVIS* 21 | setenv = 22 | TOXINIDIR={toxinidir} 23 | TOXDISTDIR={distdir} 24 | sitepackages = False 25 | whitelist_externals = env test 26 | 27 | commands = 28 | python --version 29 | virtualenv --version 30 | pip --version 31 | pip freeze 32 | py.test -rxs -vv --durations=10 --pycodestyle --flakes --blockage --cov-report term-missing --cov-report xml --cov-report html --cov-config {toxinidir}/.coveragerc --cov=pizero_gpslog {posargs} pizero_gpslog 33 | 34 | # always recreate the venv 35 | recreate = True 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | 4 | install: 5 | - virtualenv --version 6 | - git config --global user.email "travisci@jasonantman.com" 7 | - git config --global user.name "travisci" 8 | - pip install tox 9 | - pip install codecov 10 | - pip freeze 11 | - virtualenv --version 12 | 13 | script: 14 | - tox -r 15 | after_success: 16 | - codecov 17 | 18 | stages: 19 | - test 20 | - name: deploy 21 | if: tag IS present 22 | 23 | jobs: 24 | include: 25 | - stage: test 26 | python: '3.8' 27 | env: TOXENV=py38 28 | - stage: deploy 29 | python: '3.8' 30 | script: bash build_or_deploy.sh build 31 | after_success: echo after_success 32 | deploy: 33 | provider: script 34 | script: bash build_or_deploy.sh push 35 | skip_cleanup: true 36 | on: 37 | tags: true 38 | 39 | notifications: 40 | email: 41 | on_failure: always 42 | 43 | branches: 44 | except: 45 | - "/^noci-.*$/" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | bin/ 12 | include/ 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | pip-selfcheck.json 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Sphinx documentation 55 | docs/build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # virtualenv 61 | bin/ 62 | include/ 63 | 64 | .idea/ 65 | *.out -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu303-nofix.log.chk: -------------------------------------------------------------------------------- 1 | {"class":"SKY","time":"2002-11-14T14:32:54.280Z"} 2 | $GPRMC,143254,V,18000.0000,N,00000.0000,W,0.0000,0.000,141102,,*13 3 | $GPGSA,A,1,,,,,,,,,,,,,,,,*32 4 | {"class":"TPV","mode":1,"time":"2002-11-14T14:32:54.280Z","ept":0.005} 5 | {"class":"SKY","time":"2002-11-14T14:32:55.280Z","hdop":50.00} 6 | $GPRMC,143255,V,18000.0000,N,00000.0000,W,0.0000,0.000,141102,,*12 7 | $GPGSA,A,1,,,,,,,,,,,,,,,,*32 8 | {"class":"TPV","mode":1,"time":"2002-11-14T14:32:55.280Z","ept":0.005} 9 | {"class":"SKY","time":"2002-11-14T14:32:56.280Z","hdop":50.00} 10 | $GPRMC,143256,V,18000.0000,N,00000.0000,W,0.0000,0.000,141102,,*11 11 | $GPGSA,A,1,,,,,,,,,,,,,,,,*32 12 | {"class":"TPV","mode":1,"time":"2002-11-14T14:32:56.280Z","ept":0.005} 13 | {"class":"SKY","time":"2002-11-14T14:32:57.280Z","hdop":50.00} 14 | $GPRMC,143257,V,18000.0000,N,00000.0000,W,0.0000,0.000,141102,,*10 15 | $GPGSA,A,1,,,,,,,,,,,,,,,,*32 16 | {"class":"TPV","mode":1,"time":"2002-11-14T14:32:57.280Z","ept":0.005} 17 | {"class":"SKY","time":"2002-11-14T14:32:58.280Z","hdop":50.00} 18 | $GPRMC,143258,V,18000.0000,N,00000.0000,W,0.0000,0.000,141102,,*1F 19 | $GPGSA,A,1,,,,,,,,,,,,,,,,*32 20 | {"class":"TPV","mode":1,"time":"2002-11-14T14:32:58.280Z","ept":0.005} 21 | {"class":"SKY","time":"2005-06-09T14:32:59.280Z","hdop":50.00} 22 | $GPRMC,143259,V,18000.0000,N,00000.0000,W,0.0000,0.000,090605,,*13 23 | $GPGSA,A,1,,,,,,,,,,,,,,,,*32 24 | {"class":"TPV","mode":1,"time":"2005-06-09T14:32:59.280Z","ept":0.005} 25 | {"class":"SKY","time":"2005-06-09T14:33:00.280Z","hdop":50.00} 26 | $GPRMC,143300,V,18000.0000,N,00000.0000,W,0.0000,0.000,090605,,*1E 27 | $GPGSA,A,1,,,,,,,,,,,,,,,,*32 28 | {"class":"TPV","mode":1,"time":"2005-06-09T14:33:00.280Z","ept":0.005} 29 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/COPYING: -------------------------------------------------------------------------------- 1 | COPYRIGHTS 2 | 3 | Compilation copyright is held by the GPSD project. All rights reserved. 4 | 5 | GPSD project copyrights are assigned to the project lead, currently 6 | Eric S. Raymond. Other portions of the GPSD code are Copyright (c) 7 | 1997, 1998, 1999, 2000, 2001, 2002 by Remco Treffkorn, and others 8 | Copyright (c) 2005 by Eric S. Raymond. For other copyrights, see 9 | individual files. 10 | 11 | BSD LICENSE 12 | 13 | Redistribution and use in source and binary forms, with or without 14 | modification, are permitted provided that the following conditions 15 | are met:

16 | 17 | Redistributions of source code must retain the above copyright 18 | notice, this list of conditions and the following disclaimer.

19 | 20 | Redistributions in binary form must reproduce the above copyright 21 | notice, this list of conditions and the following disclaimer in the 22 | documentation and/or other materials provided with the distribution.

23 | 24 | Neither name of the GPSD project nor the names of its contributors 25 | may be used to endorse or promote products derived from this software 26 | without specific prior written permission. 27 | 28 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 29 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 30 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 31 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR 32 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 33 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 34 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 35 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 36 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 37 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 38 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | -------------------------------------------------------------------------------- /pizero_gpslog/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | -------------------------------------------------------------------------------- /pizero_gpslog/displays/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | -------------------------------------------------------------------------------- /pizero_gpslog/extradata/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | -------------------------------------------------------------------------------- /pizero_gpslog/utils.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | from copy import copy 3 | import logging 4 | from enum import Enum 5 | 6 | 7 | class ThreadSafeValue: 8 | 9 | def __init__(self, initial=''): 10 | self._value = initial 11 | self._lock = Lock() 12 | 13 | def set(self, val): 14 | self._lock.acquire() 15 | try: 16 | self._value = copy(val) 17 | finally: 18 | self._lock.release() 19 | 20 | def get(self): 21 | self._lock.acquire() 22 | try: 23 | result = copy(self._value) 24 | finally: 25 | self._lock.release() 26 | return result 27 | 28 | 29 | class FixType(Enum): 30 | 31 | NO_GPS = 0 32 | NO_FIX = 1 33 | FIX_2D = 2 34 | FIX_3D = 3 35 | 36 | 37 | def set_log_info(log: logging.Logger): 38 | """ 39 | set logger level to INFO via :py:func:`~.set_log_level_format`. 40 | """ 41 | set_log_level_format( 42 | log, logging.INFO, 43 | '%(asctime)s %(levelname)s:%(name)s:%(message)s' 44 | ) 45 | 46 | 47 | def set_log_debug(log: logging.Logger): 48 | """ 49 | set logger level to DEBUG, and debug-level output format, 50 | via :py:func:`~.set_log_level_format`. 51 | """ 52 | set_log_level_format( 53 | log, 54 | logging.DEBUG, 55 | "%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - " 56 | "%(name)s.%(funcName)s() ] %(message)s" 57 | ) 58 | 59 | 60 | def set_log_level_format(log: logging.Logger, level: int, format: str): 61 | """ 62 | Set logger level and format. 63 | 64 | :param logger: the logger object to set on 65 | :type logger: logging.Logger 66 | :param level: logging level; see the :py:mod:`logging` constants. 67 | :type level: int 68 | :param format: logging formatter format string 69 | :type format: str 70 | """ 71 | formatter = logging.Formatter(fmt=format) 72 | log.handlers[0].setFormatter(formatter) 73 | log.setLevel(level) 74 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.1.0 (2020-09-11) 5 | ------------------ 6 | 7 | * Fix ``pizero_gpslog.extradata.gq_gmc500plus:GqGMC500plus`` to handle disconnect/reconnect of USB device. 8 | * Add support for the Adafruit 4567 OLED display. 9 | * Major refactor of display support; instead of taking a list of lines, ``BaseDisplay`` subclasses now take separate values for each piece of data that can be displayed and are free to format them however works best for that particular display. 10 | 11 | 1.0.0 (2020-08-25) 12 | ------------------ 13 | 14 | * Update README for current Raspberry Pi OS install instructions 15 | * Add ``setup_pi.sh`` setup script, based on the one at https://github.com/jantman/pi2graphite/blob/master/setup_raspbian.sh 16 | * Add development documentation 17 | * Add support for displays. Begin with the Waveshare 2.13-inch e-Ink Display Hat B; allow users to implement their own display driver classes. 18 | * Implement support for extra data providers. 19 | 20 | 0.1.4 (2020-07-23) 21 | ------------------ 22 | 23 | * Merge `PR #5 `__ to have converter ignore corrupt lines. Thanks to `@markus-k `__. 24 | * Use TravisCI for releases; document release process 25 | * PEP8 fixes 26 | 27 | 0.1.3 (2019-12-01) 28 | ------------------ 29 | 30 | * Refactor ``pizero-gpslog-convert`` to allow use from other Python programs. 31 | 32 | 0.1.2 (2018-11-03) 33 | ------------------ 34 | 35 | * ``pizero-gpslog-convert`` - Handle logs that are missing altitude (``alt``) from TPV 36 | reports by falling back to GST altitude, the previous altitude measurement, or 0.0 (in that order). 37 | 38 | 0.1.1 (2018-04-08) 39 | ------------------ 40 | 41 | * Log version at startup 42 | * RPi RTC fix - don't start writing output until we have a fix; use GPS time instead of system time for filename 43 | * Numerous bugfixes in ``converter.py`` 44 | * README fixes 45 | 46 | 0.1.0 (2018-03-06) 47 | ------------------ 48 | 49 | * Initial Release 50 | -------------------------------------------------------------------------------- /pizero_gpslog/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | 39 | VERSION = '1.1.0' 40 | PROJECT_URL = 'https://github.com/jantman/pizero-gpslog' 41 | -------------------------------------------------------------------------------- /pizero_gpslog/extradata/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | from abc import ABC, abstractmethod 39 | import logging 40 | from threading import Thread 41 | 42 | logger = logging.getLogger(__name__) 43 | 44 | 45 | class BaseExtraDataProvider(ABC, Thread): 46 | """ 47 | Base class for all extra data providers. 48 | 49 | ``self._data`` should be a dict with a ``message`` key that has a string 50 | value, and a ``data`` key that has an arbitrary JSON-encodable value. 51 | """ 52 | 53 | def __init__(self): 54 | self._data = {} 55 | super().__init__(name='ExtraDataProvider', daemon=True) 56 | 57 | @property 58 | def data(self): 59 | return self._data 60 | 61 | @abstractmethod 62 | def run(self): 63 | raise NotImplementedError() 64 | -------------------------------------------------------------------------------- /pizero_gpslog/extradata/dummy.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | import logging 39 | from pizero_gpslog.extradata.base import BaseExtraDataProvider 40 | from time import time, sleep 41 | from datetime import datetime 42 | 43 | logger = logging.getLogger(__name__) 44 | 45 | 46 | class DummyData(BaseExtraDataProvider): 47 | 48 | def __init__(self): 49 | super().__init__() 50 | 51 | def run(self): 52 | logger.debug('Running DummyData extra data provider...') 53 | while True: 54 | dt = datetime.now() 55 | hms = dt.strftime('%H:%M:%W') 56 | self._data = { 57 | 'message': f'updated at {hms}', 58 | 'data': { 59 | 'hms': hms, 60 | 'time': time() 61 | } 62 | } 63 | sleep(2) 64 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/test_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | import pizero_gpslog.version as version 39 | 40 | import re 41 | 42 | 43 | class TestVersion(object): 44 | 45 | def test_project_url(self): 46 | expected = 'https://github.com/jantman/pizero-gpslog' 47 | assert version.PROJECT_URL == expected 48 | 49 | def test_is_semver(self): 50 | # see: 51 | # https://github.com/mojombo/semver.org/issues/59#issuecomment-57884619 52 | semver_ptn = re.compile( 53 | r'^' 54 | r'(?P(?:' 55 | r'0|(?:[1-9]\d*)' 56 | r'))' 57 | r'\.' 58 | r'(?P(?:' 59 | r'0|(?:[1-9]\d*)' 60 | r'))' 61 | r'\.' 62 | r'(?P(?:' 63 | r'0|(?:[1-9]\d*)' 64 | r'))' 65 | r'(?:-(?P' 66 | r'[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*' 67 | r'))?' 68 | r'(?:\+(?P' 69 | r'[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*' 70 | r'))?' 71 | r'$' 72 | ) 73 | assert semver_ptn.match(version.VERSION) is not None 74 | -------------------------------------------------------------------------------- /pizero_gpslog/fakeled.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################ 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################ 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################ 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################ 36 | """ 37 | 38 | import logging 39 | 40 | logger = logging.getLogger(__name__) 41 | 42 | 43 | class FakeLed(object): 44 | 45 | def __init__(self, pin_num, **kwargs): 46 | self.pin_num = pin_num 47 | self._lit = False 48 | 49 | def on(self): 50 | logger.info('%s ON' % self) 51 | self._lit = True 52 | 53 | def off(self): 54 | logger.info('%s OFF' % self) 55 | self._lit = False 56 | 57 | def blink(self, on_time=1, off_time=1, n=None, background=True): 58 | if n is None: 59 | raise RuntimeError('ERROR: method would never return!') 60 | if not background: 61 | raise RuntimeError( 62 | 'ERROR: LED.blink not called from background!' 63 | ) 64 | logger.info('%s BLINK on=%s off=%s n=%s background=%s', 65 | self, on_time, off_time, n, background) 66 | self._lit = False 67 | 68 | def toggle(self): 69 | logger.info('%s TOGGLE' % self) 70 | self._lit = not self._lit 71 | 72 | @property 73 | def is_lit(self): 74 | return self._lit 75 | 76 | @property 77 | def pin(self): 78 | return self.pin_num 79 | 80 | def __repr__(self): 81 | return '' % self.pin_num 82 | -------------------------------------------------------------------------------- /pizero_gpslog/screentest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | The latest version of this package is available at: 5 | 6 | 7 | ################################################################################## 8 | Copyright 2018-2020 Jason Antman 9 | 10 | This file is part of pizero-gpslog, also known as pizero-gpslog. 11 | 12 | pizero-gpslog is free software: you can redistribute it and/or modify 13 | it under the terms of the GNU Affero General Public License as published by 14 | the Free Software Foundation, either version 3 of the License, or 15 | (at your option) any later version. 16 | 17 | pizero-gpslog is distributed in the hope that it will be useful, 18 | but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | GNU Affero General Public License for more details. 21 | 22 | You should have received a copy of the GNU Affero General Public License 23 | along with pizero-gpslog. If not, see . 24 | 25 | The Copyright and Authors attributions contained herein may not be removed or 26 | otherwise altered, except to add the Author attribution of a contributor to 27 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 28 | ################################################################################## 29 | While not legally required, I sincerely request that anyone who finds 30 | bugs please submit them at or 31 | to me via email, and that you send any contributions or improvements 32 | either as a pull request on GitHub, or to me via email. 33 | ################################################################################## 34 | 35 | AUTHORS: 36 | Jason Antman 37 | ################################################################################## 38 | """ 39 | 40 | import os 41 | import logging 42 | import time 43 | from datetime import datetime 44 | from pizero_gpslog.displaymanager import DisplayManager 45 | from pizero_gpslog.utils import set_log_debug 46 | 47 | logging.basicConfig(level=logging.DEBUG) 48 | logger = logging.getLogger() 49 | set_log_debug(logger) 50 | 51 | 52 | def main(): 53 | if 'DISPLAY_CLASS' not in os.environ: 54 | logger.warning( 55 | 'DISPLAY_CLASS environment variable not set; using default dummy' 56 | ) 57 | os.environ[ 58 | 'DISPLAY_CLASS' 59 | ] = 'pizero_gpslog.displays.dummy:DummyDisplay' 60 | modname, clsname = os.environ['DISPLAY_CLASS'].split(':') 61 | dm = DisplayManager(modname, clsname) 62 | dm.start() 63 | for i in range(0, 10): 64 | logger.info('OUTER sleep 5s') 65 | time.sleep(5) 66 | logger.info('OUTER set display') 67 | dm.set_heading(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) 68 | dm.set_status('A' + (f'{i}' * 19)) 69 | dm.set_lat('B' + (f'{i}' * 19)) 70 | dm.set_lon('C' + (f'{i}' * 19)) 71 | dm.set_extradata('D' + (f'{i}' * 19)) 72 | logger.info('OUTER Finished display-setting loop') 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | from setuptools import setup, find_packages 39 | from pizero_gpslog.version import VERSION, PROJECT_URL 40 | 41 | with open('README.rst') as file: 42 | long_description = file.read() 43 | 44 | requires = [ 45 | 'gpiozero', 46 | 'gpxpy', 47 | 'pint', 48 | 'pillow' 49 | ] 50 | 51 | classifiers = [ 52 | 'Development Status :: 5 - Production/Stable', 53 | 'Environment :: No Input/Output (Daemon)', 54 | 'Intended Audience :: End Users/Desktop', 55 | 'Natural Language :: English', 56 | 'Operating System :: POSIX :: Linux', 57 | 'Topic :: Other/Nonlisted Topic', 58 | 'Topic :: System :: Logging', 59 | 'Topic :: Utilities', 60 | 'License :: OSI Approved :: GNU Affero General Public License ' 61 | 'v3 or later (AGPLv3+)', 62 | 'Programming Language :: Python :: 3 :: Only', 63 | 'Programming Language :: Python :: 3.7', 64 | 'Programming Language :: Python :: 3.8', 65 | ] 66 | 67 | setup( 68 | name='pizero-gpslog', 69 | version=VERSION, 70 | author='Jason Antman', 71 | author_email='jason@jasonantman.com', 72 | packages=find_packages(), 73 | url=PROJECT_URL, 74 | description='Raspberry Pi Zero gpsd logger with status LEDs.', 75 | long_description=long_description, 76 | install_requires=requires, 77 | entry_points=""" 78 | [console_scripts] 79 | pizero-gpslog = pizero_gpslog.runner:main 80 | pizero-gpslog-install = pizero_gpslog.installer:main 81 | pizero-gpslog-convert = pizero_gpslog.converter:main 82 | pizero-gpslog-screentest = pizero_gpslog.screentest:main 83 | """, 84 | keywords="raspberry pi rpi gps log logger gpsd", 85 | classifiers=classifiers, 86 | include_package_data=True, 87 | package_data={ 88 | 'pizero_gpslog': ['*.ttf'] 89 | } 90 | ) 91 | -------------------------------------------------------------------------------- /pizero_gpslog/displays/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | from abc import ABC, abstractmethod 39 | import logging 40 | from PIL import ImageFont 41 | from pkg_resources import resource_filename 42 | from typing import ClassVar, Tuple 43 | from pizero_gpslog.utils import FixType 44 | from datetime import datetime 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | 49 | class BaseDisplay(ABC): 50 | """ 51 | Base class for all displays. 52 | """ 53 | 54 | #: width of the display in characters 55 | width_chars: ClassVar[int] = 0 56 | 57 | #: height of the display in lines 58 | height_lines: ClassVar[int] = 0 59 | 60 | #: the minimum number of seconds between refreshes of the display 61 | min_refresh_seconds: ClassVar[int] = 0 62 | 63 | def __init__(self): 64 | """ 65 | Initialize the display. This should do everything that's required to 66 | get the display ready to call :py:meth:`~.display`, including clearing 67 | the display if required. 68 | """ 69 | pass 70 | 71 | @staticmethod 72 | def font(size_pts: int = 20) -> ImageFont.FreeTypeFont: 73 | f = resource_filename('pizero_gpslog', 'DejaVuSansMono.ttf') 74 | return ImageFont.truetype(f, size_pts) 75 | 76 | @abstractmethod 77 | def update_display( 78 | self, fix_type: FixType, lat: float, lon: float, extradata: str, 79 | fix_precision: Tuple[float, float], dt: datetime, should_clear: bool 80 | ): 81 | """ 82 | Write ``self._lines`` to the display. 83 | """ 84 | raise NotImplementedError() 85 | 86 | @abstractmethod 87 | def clear(self): 88 | """ 89 | Clear the display. 90 | """ 91 | raise NotImplementedError() 92 | 93 | @abstractmethod 94 | def __del__(self): 95 | """ 96 | Do everything that is needed to cleanup the display. 97 | """ 98 | raise NotImplementedError() 99 | -------------------------------------------------------------------------------- /setup_pi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Raspbian setup script for https://github.com/jantman/pi2graphite 3 | 4 | # check arguments 5 | if [[ "$1" == "-h" || "$1" == "--help" ]]; then 6 | echo "USAGE: setup_pi.sh BASEDEV HOSTNAME AUTHKEYS_PATH [SSID PSK]" 7 | echo "where BASEDEV is the base device, e.g. '/dev/sdX'" 8 | echo "where AUTHKEYS_PATH is the path to an authorized_keys file to copy to the pi user" 9 | exit 0 10 | fi 11 | 12 | if [[ "$#" -ne 3 && "$#" -ne 5 ]]; then 13 | echo "ERROR: wrong number of arguments (see --help)" >&2 14 | exit 1 15 | fi 16 | 17 | # check for root 18 | if [[ "$EUID" != "0" ]]; then 19 | echo "ERROR: this script must be run as root" >&2 20 | exit 1 21 | fi 22 | 23 | # assign to meaningful var names 24 | BASEDEV=$1 25 | HOSTNAME=$2 26 | AUTHKEYS_PATH=$3 27 | SSID=$4 28 | PSK=$5 29 | COUNTRY="US" # WiFi Regulatory Domain 30 | 31 | # check that partitions look right 32 | if [[ ! -e "${BASEDEV}1" ]]; then 33 | echo "ERROR: ${BASEDEV}1 does not exist; wrong BASEDEV or bad partition layout" >&2 34 | exit 1 35 | fi 36 | 37 | if [[ ! -e "${BASEDEV}2" ]]; then 38 | echo "ERROR: ${BASEDEV}2 does not exist; wrong BASEDEV or bad partition layout" >&2 39 | exit 1 40 | fi 41 | 42 | if [[ -e "${BASEDEV}3" ]]; then 43 | echo "ERROR: ${BASEDEV}3 exists; wrong BASEDEV or bad partition layout" >&2 44 | exit 1 45 | fi 46 | 47 | if [[ ! -f $AUTHKEYS_PATH ]]; then 48 | echo "ERROR: AUTHKEYS_PATH (${AUTHKEYS_PATH}) does not exist" >&2 49 | exit 1 50 | fi 51 | 52 | # echo out settings 53 | echo "Starting Raspbian configuration" 54 | echo "Base device: $BASEDEV" 55 | echo "Hostname: $HOSTNAME" 56 | echo "Authorized keys file: $AUTHKEYS_PATH" 57 | if [ -n "$SSID" ]; then 58 | echo "SSID: $SSID" 59 | echo "WiFi Key: $PSK" 60 | echo "WiFi Country: $COUNTRY" 61 | fi 62 | 63 | # make sure it looks right to the user 64 | read -p "Does this look right? [y|N]" response 65 | if [[ "$response" != "y" ]]; then 66 | echo "Ok, exiting." >&2 67 | exit 1 68 | fi 69 | 70 | # create a temporary directory 71 | TMPDIR=$(mktemp -d) 72 | 73 | # error handling 74 | cleanup() { 75 | echo "Syncing disks" 76 | sync 77 | echo "Unmounting $TMPDIR" 78 | umount --recursive $TMPDIR 79 | echo "Removing $TMPDIR" 80 | rm -Rf $TMPDIR 81 | } 82 | trap cleanup 0 83 | 84 | echo "Mounting SD card" 85 | mount "${BASEDEV}2" $TMPDIR 86 | mount "${BASEDEV}1" "${TMPDIR}/boot" 87 | 88 | echo "Done mounting SD card; chroot'ing" 89 | 90 | echo "Touching boot/ssh" 91 | touch "${TMPDIR}/boot/ssh" 92 | 93 | PI_UID=$(stat --printf='%u' "${TMPDIR}/home/pi") 94 | PI_GID=$(stat --printf='%g' "${TMPDIR}/home/pi") 95 | echo "Found pi user; UID=${PI_UID} GID=${PI_GID}" 96 | 97 | if [[ ! -e "${TMPDIR}/home/pi/.ssh" ]]; then 98 | echo "Creating pi user .ssh directory" 99 | install -d -m 0700 -o $PI_UID -g $PI_GID "${TMPDIR}/home/pi/.ssh" 100 | fi 101 | 102 | AKPATH="${TMPDIR}/home/pi/.ssh/authorized_keys" 103 | echo "Copying authorized keys file (${AUTHKEYS_PATH}) to $AKPATH" 104 | install -m 0644 -o $PI_UID -g $PI_GID $AUTHKEYS_PATH $AKPATH 105 | 106 | echo "Setting hostname" 107 | echo $HOSTNAME > "${TMPDIR}/etc/hostname" 108 | sed -i "s/raspberrypi/${HOSTNAME}/g" "${TMPDIR}/etc/hosts" 109 | 110 | if [ -n "$SSID" ]; then 111 | echo "Setting up WiFi configuration..." 112 | if ! grep "country=${COUNTRY}" "${TMPDIR}/etc/wpa_supplicant/wpa_supplicant.conf" &>/dev/null; then 113 | echo "Setting country code" 114 | sed -i "s/country=.*/country=${COUNTRY}/" "${TMPDIR}/etc/wpa_supplicant/wpa_supplicant.conf" 115 | fi 116 | netconf=$(cat </dev/null; then 124 | echo "Adding network configuration" 125 | echo "$netconf" >> "${TMPDIR}/etc/wpa_supplicant/wpa_supplicant.conf" 126 | fi 127 | fi 128 | -------------------------------------------------------------------------------- /pizero_gpslog/displays/dummy.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | import logging 39 | from pizero_gpslog.displays.base import BaseDisplay 40 | from pizero_gpslog.utils import FixType 41 | from typing import ClassVar, Tuple 42 | from datetime import datetime 43 | import time 44 | import os 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | 49 | class DummyDisplay(BaseDisplay): 50 | 51 | #: width of the display in characters 52 | width_chars: ClassVar[int] = 21 53 | 54 | #: height of the display in lines 55 | height_lines: ClassVar[int] = 5 56 | 57 | #: the minimum number of seconds between refreshes of the display 58 | min_refresh_seconds: ClassVar[int] = 15 59 | 60 | def __init__(self): 61 | super().__init__() 62 | self.sleep_time: int = int(os.environ.get('DUMMY_SLEEP_TIME', '2')) 63 | logger.debug( 64 | 'Initialize DummyDisplay; sleep time (%d sec) set by ' 65 | 'DUMMY_SLEEP_TIME environment variable.' 66 | ) 67 | 68 | def update_display( 69 | self, fix_type: FixType, lat: float, lon: float, extradata: str, 70 | fix_precision: Tuple[float, float], dt: datetime, should_clear: bool 71 | ): 72 | if should_clear: 73 | self.clear() 74 | lines = [dt.strftime('%H:%M:%S UTC')] 75 | if fix_type == FixType.NO_GPS: 76 | lines.extend(['No GPS yet', '', '']) 77 | elif fix_type == FixType.NO_FIX: 78 | lines.extend(['No Fix yet', '', '']) 79 | else: 80 | ft = '??' 81 | if fix_type == FixType.FIX_2D: 82 | ft = '2D' 83 | elif fix_type == FixType.FIX_3D: 84 | ft = '3D' 85 | lines.append(f'{ft} {fix_precision[0]:.8},{fix_precision[1]:.8}') 86 | lines.append(f'Lat: {lat:.15}') 87 | lines.append(f'Lon: {lon:.15}') 88 | lines.append(extradata) 89 | self._write_lines(lines) 90 | 91 | def _write_lines(self, lines): 92 | fmt: str = 'DUMMYDISPLAY>|%-' + '%ds|' % self.width_chars 93 | for line in lines: 94 | logger.warning(fmt, line) 95 | logger.debug('Dummy display sleeping %d seconds...', self.sleep_time) 96 | time.sleep(self.sleep_time) 97 | 98 | def clear(self): 99 | logger.warning('------ DUMMYDISPLAY CLEAR -------') 100 | 101 | def __del__(self): 102 | logger.warning('DUMMYDISPLAY __del__') 103 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu353s4.log: -------------------------------------------------------------------------------- 1 | # Name: GlobalSat BU-353-S4 2 | # Chipset = SiRF-IV 3 | # Firmware = ?-GSD4e_4.0.4-P1_RPATCH.07-GS008-F-GPS-4R-1201128 01/12/2012 012 4 | # Date = 2014-12-31 5 | # Submitter = Paul Beard 6 | # Location = Hamilton, Waikato, 37.8S 175.3E 7 | $GPGGA,030719.000,3747.0873,S,17518.8938,E,1,08,1.1,59.9,M,23.7,M,,0000*7D 8 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 9 | $GPRMC,030719.000,A,3747.0873,S,17518.8938,E,0.69,181.39,311214,,,A*7D 10 | $GPGGA,030720.000,3747.0875,S,17518.8938,E,1,08,1.1,59.8,M,23.7,M,,0000*70 11 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 12 | $GPRMC,030720.000,A,3747.0875,S,17518.8938,E,1.15,181.39,311214,,,A*7B 13 | $GPGGA,030721.000,3747.0876,S,17518.8934,E,1,08,1.1,60.1,M,23.7,M,,0000*7D 14 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 15 | $GPGSV,3,1,12,17,72,207,32,28,59,095,31,06,35,347,25,01,29,132,23*7B 16 | $GPGSV,3,2,12,30,23,011,29,20,17,069,20,26,09,293,23,13,07,304,10*7C 17 | $GPGSV,3,3,12,11,12,132,04,08,38,008,,02,35,348,,03,14,091,*73 18 | $GPRMC,030721.000,A,3747.0876,S,17518.8934,E,0.89,181.39,311214,,,A*71 19 | $GPGGA,030722.000,3747.0877,S,17518.8933,E,1,08,1.1,60.1,M,23.7,M,,0000*78 20 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 21 | $GPRMC,030722.000,A,3747.0877,S,17518.8933,E,0.12,181.39,311214,,,A*76 22 | $GPGGA,030723.000,3747.0878,S,17518.8935,E,1,08,1.1,60.0,M,23.7,M,,0000*71 23 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 24 | $GPRMC,030723.000,A,3747.0878,S,17518.8935,E,0.66,181.39,311214,,,A*7D 25 | $GPGGA,030724.000,3747.0878,S,17518.8936,E,1,08,1.1,60.1,M,23.7,M,,0000*74 26 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 27 | $GPRMC,030724.000,A,3747.0878,S,17518.8936,E,0.32,181.39,311214,,,A*78 28 | $GPGGA,030725.000,3747.0878,S,17518.8936,E,1,08,1.1,60.2,M,23.7,M,,0000*76 29 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 30 | $GPRMC,030725.000,A,3747.0878,S,17518.8936,E,0.00,181.39,311214,,,A*78 31 | $GPGGA,030726.000,3747.0877,S,17518.8936,E,1,08,1.1,60.6,M,23.7,M,,0000*7E 32 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 33 | $GPGSV,3,1,12,17,72,207,32,28,59,095,30,06,35,347,25,01,29,132,23*7A 34 | $GPGSV,3,2,12,30,23,011,29,20,17,069,20,26,09,293,23,13,07,304,10*7C 35 | $GPGSV,3,3,12,11,12,132,04,08,38,008,,02,35,348,,03,14,091,*73 36 | $GPRMC,030726.000,A,3747.0877,S,17518.8936,E,0.59,181.39,311214,,,A*78 37 | $GPGGA,030727.000,3747.0877,S,17518.8938,E,1,08,1.1,60.8,M,23.7,M,,0000*7F 38 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 39 | $GPRMC,030727.000,A,3747.0877,S,17518.8938,E,0.29,181.39,311214,,,A*70 40 | $GPGGA,030728.000,3747.0878,S,17518.8940,E,1,08,1.1,60.5,M,23.7,M,,0000*7D 41 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 42 | $GPRMC,030728.000,A,3747.0878,S,17518.8940,E,0.52,181.39,311214,,,A*73 43 | $GPGGA,030729.000,3747.0879,S,17518.8942,E,1,08,1.1,60.7,M,23.7,M,,0000*7D 44 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 45 | $GPRMC,030729.000,A,3747.0879,S,17518.8942,E,0.48,181.39,311214,,,A*7A 46 | $GPGGA,030730.000,3747.0878,S,17518.8942,E,1,07,1.1,60.5,M,23.7,M,,0000*79 47 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 48 | $GPRMC,030730.000,A,3747.0878,S,17518.8942,E,0.72,181.39,311214,,,A*7A 49 | $GPGGA,030731.000,3747.0877,S,17518.8941,E,1,07,1.1,60.5,M,23.7,M,,0000*74 50 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 51 | $GPGSV,3,1,12,17,71,207,32,28,59,095,30,06,36,347,25,01,29,132,23*7A 52 | $GPGSV,3,2,12,30,23,011,29,20,17,070,19,26,08,293,24,13,07,305,07*7F 53 | $GPGSV,3,3,12,08,38,008,,02,35,348,,03,15,091,,24,12,219,*7A 54 | $GPRMC,030731.000,A,3747.0877,S,17518.8941,E,0.47,181.39,311214,,,A*71 55 | $GPGGA,030732.000,3747.0876,S,17518.8940,E,1,07,1.1,60.4,M,23.7,M,,0000*76 56 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 57 | $GPRMC,030732.000,A,3747.0876,S,17518.8940,E,0.43,181.39,311214,,,A*76 58 | $GPGGA,030733.000,3747.0876,S,17518.8941,E,1,07,1.1,60.2,M,23.7,M,,0000*70 59 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 60 | $GPRMC,030733.000,A,3747.0876,S,17518.8941,E,0.23,181.39,311214,,,A*70 61 | $GPGGA,030734.000,3747.0876,S,17518.8941,E,1,07,1.1,60.0,M,23.7,M,,0000*75 62 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 63 | $GPRMC,030734.000,A,3747.0876,S,17518.8941,E,0.51,181.39,311214,,,A*72 64 | $GPGGA,030735.000,3747.0876,S,17518.8941,E,1,07,1.1,60.0,M,23.7,M,,0000*74 65 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 66 | $GPRMC,030735.000,A,3747.0876,S,17518.8941,E,0.00,181.39,311214,,,A*77 67 | $GPGGA,030736.000,3747.0876,S,17518.8941,E,1,07,1.1,60.0,M,23.7,M,,0000*77 68 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 69 | $GPGSV,3,1,12,17,71,207,32,28,59,095,30,06,36,347,25,01,29,132,23*7A 70 | $GPGSV,3,2,12,30,23,011,29,20,17,070,19,26,08,293,24,13,07,305,04*7C 71 | $GPGSV,3,3,12,08,38,008,,02,35,348,,03,15,091,,24,12,219,*7A 72 | $GPRMC,030736.000,A,3747.0876,S,17518.8941,E,0.00,181.39,311214,,,A*74 73 | $GPGGA,030737.000,3747.0877,S,17518.8942,E,1,07,1.1,60.0,M,23.7,M,,0000*74 74 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 75 | $GPRMC,030737.000,A,3747.0877,S,17518.8942,E,0.25,181.39,311214,,,A*70 76 | $GPGGA,030738.000,3747.0877,S,17518.8942,E,1,07,1.1,60.0,M,23.7,M,,0000*7B 77 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 78 | $GPRMC,030738.000,A,3747.0877,S,17518.8942,E,0.00,181.39,311214,,,A*78 79 | $GPGGA,030739.000,3747.0877,S,17518.8942,E,1,07,1.1,60.0,M,23.7,M,,0000*7A 80 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 81 | $GPRMC,030739.000,A,3747.0877,S,17518.8942,E,0.00,181.39,311214,,,A*79 82 | $GPGGA,030740.000,3747.0877,S,17518.8942,E,1,07,1.1,60.2,M,23.7,M,,0000*76 83 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 84 | $GPRMC,030740.000,A,3747.0877,S,17518.8942,E,0.26,181.39,311214,,,A*73 85 | $GPGGA,030741.000,3747.0877,S,17518.8941,E,1,07,1.1,60.2,M,23.7,M,,0000*74 86 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 87 | $GPGSV,3,1,12,17,71,207,31,28,59,095,30,06,36,347,26,01,29,132,23*7A 88 | $GPGSV,3,2,12,30,23,011,29,20,17,070,18,26,08,293,24,13,07,305,04*7D 89 | $GPGSV,3,3,12,08,38,008,,02,35,348,,03,15,091,,24,12,219,*7A 90 | $GPRMC,030741.000,A,3747.0877,S,17518.8941,E,0.00,181.39,311214,,,A*75 91 | $GPGGA,030742.000,3747.0877,S,17518.8941,E,1,07,1.1,60.2,M,23.7,M,,0000*77 92 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 93 | $GPRMC,030742.000,A,3747.0877,S,17518.8941,E,0.00,181.39,311214,,,A*76 94 | $GPGGA,030743.000,3747.0877,S,17518.8941,E,1,07,1.1,60.2,M,23.7,M,,0000*76 95 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 96 | $GPRMC,030743.000,A,3747.0877,S,17518.8941,E,0.00,181.39,311214,,,A*77 97 | -------------------------------------------------------------------------------- /pizero_gpslog/displays/adafruit4567.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modified from: 3 | https://github.com/waveshare/e-Paper/blob/ 4 | 717cbb8d9215e58f9f3cdde45ee329f516504afe/RaspberryPi%26JetsonNano/python/ 5 | lib/waveshare_epd/epd2in13bc.py 6 | 7 | Display driver class for Waveshare e-Paper Display HAT 2.13 inch (B) 8 | 9 | https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT_(B) 10 | https://www.amazon.com/gp/product/B075FR81WL/ 11 | 12 | The latest version of this package is available at: 13 | 14 | 15 | ################################################################################## 16 | Copyright 2018-2020 Jason Antman 17 | 18 | This file is part of pizero-gpslog, also known as pizero-gpslog. 19 | 20 | pizero-gpslog is free software: you can redistribute it and/or modify 21 | it under the terms of the GNU Affero General Public License as published by 22 | the Free Software Foundation, either version 3 of the License, or 23 | (at your option) any later version. 24 | 25 | pizero-gpslog is distributed in the hope that it will be useful, 26 | but WITHOUT ANY WARRANTY; without even the implied warranty of 27 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 28 | GNU Affero General Public License for more details. 29 | 30 | You should have received a copy of the GNU Affero General Public License 31 | along with pizero-gpslog. If not, see . 32 | 33 | The Copyright and Authors attributions contained herein may not be removed or 34 | otherwise altered, except to add the Author attribution of a contributor to 35 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 36 | ################################################################################## 37 | While not legally required, I sincerely request that anyone who finds 38 | bugs please submit them at or 39 | to me via email, and that you send any contributions or improvements 40 | either as a pull request on GitHub, or to me via email. 41 | ################################################################################## 42 | 43 | AUTHORS: 44 | Jason Antman 45 | ################################################################################## 46 | """ 47 | 48 | import logging 49 | from typing import ClassVar, Tuple 50 | from board import SCL, SDA, D4 51 | import busio 52 | import digitalio 53 | import adafruit_ssd1305 54 | from pizero_gpslog.displays.base import BaseDisplay 55 | from pizero_gpslog.utils import FixType 56 | from PIL import Image, ImageDraw 57 | from datetime import datetime 58 | 59 | logger = logging.getLogger(__name__) 60 | 61 | 62 | class Adafruit4567(BaseDisplay): 63 | 64 | #: width of the display in characters 65 | width_chars: ClassVar[int] = 20 66 | 67 | #: height of the display in lines 68 | height_lines: ClassVar[int] = 4 69 | 70 | #: the minimum number of seconds between refreshes of the display 71 | min_refresh_seconds: ClassVar[int] = 0.1 72 | 73 | def __init__(self): 74 | super().__init__() 75 | self._oled_reset = digitalio.DigitalInOut(D4) 76 | self._i2c = busio.I2C(SCL, SDA) 77 | # Create the SSD1305 OLED class. 78 | # The first two parameters are the pixel width and pixel height. 79 | # Change these to the right size for your display! 80 | self._disp = adafruit_ssd1305.SSD1305_I2C( 81 | 128, 32, self._i2c, reset=self._oled_reset 82 | ) 83 | self.clear() 84 | # Create blank image for drawing. 85 | # Make sure to create image with mode '1' for 1-bit color. 86 | self._width = self._disp.width 87 | self._height = self._disp.height 88 | self._image = Image.new("1", (self._width, self._height)) 89 | # Get drawing object to draw on image. 90 | self._draw = ImageDraw.Draw(self._image) 91 | # Draw a black filled box to clear the image. 92 | self._draw.rectangle( 93 | (0, 0, self._width, self._height), outline=0, fill=0 94 | ) 95 | self._top = -2 96 | self._font = BaseDisplay.font(8) 97 | 98 | def update_display( 99 | self, fix_type: FixType, lat: float, lon: float, extradata: str, 100 | fix_precision: Tuple[float, float], dt: datetime, should_clear: bool 101 | ): 102 | if should_clear: 103 | self.clear() 104 | dts = dt.strftime('%H:%M:%S Z') 105 | if fix_type == FixType.NO_GPS: 106 | return self._write_lines([ 107 | dts, 108 | 'No GPS yet', 109 | '', 110 | extradata 111 | ]) 112 | if fix_type == FixType.NO_FIX: 113 | return self._write_lines([ 114 | dts, 115 | 'No Fix yet', 116 | '', 117 | extradata 118 | ]) 119 | ft = '??' 120 | if fix_type == FixType.FIX_2D: 121 | ft = '2D' 122 | elif fix_type == FixType.FIX_3D: 123 | ft = '3D' 124 | if extradata is not None and extradata.strip() != '': 125 | return self._write_lines([ 126 | dts + f' | {ft} fix', 127 | f'Lat: {lat:.15}', 128 | f'Lon: {lon:.15}', 129 | extradata 130 | ]) 131 | # else we don't have extradata, so we have an extra line... 132 | return self._write_lines([ 133 | dts, 134 | f'{ft} fix: {fix_precision[0]:.7},{fix_precision[1]:.7}', 135 | f'Lat: {lat:.15}', 136 | f'Lon: {lon:.15}' 137 | ]) 138 | 139 | def _write_lines(self, lines): 140 | logging.info('Begin update display') 141 | self._draw.rectangle( 142 | (0, 0, self._width, self._height), outline=0, fill=0 143 | ) 144 | for idx, content in enumerate(lines): 145 | coords = (0, self._top + (idx * 8)) 146 | self._draw.text( 147 | coords, content, font=self._font, fill=255 148 | ) 149 | # Display image. 150 | self._disp.image(self._image) 151 | self._disp.show() 152 | logging.info('End update display') 153 | 154 | def clear(self): 155 | self._disp.fill(0) 156 | self._disp.show() 157 | 158 | def __del__(self): 159 | self.clear() 160 | -------------------------------------------------------------------------------- /pizero_gpslog/displaymanager.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | import os 39 | import logging 40 | from importlib import import_module 41 | import time 42 | from threading import Thread 43 | from typing import Optional, Tuple 44 | from datetime import datetime, timezone 45 | from pizero_gpslog.displays.base import BaseDisplay 46 | from pizero_gpslog.utils import ThreadSafeValue, FixType 47 | 48 | logger = logging.getLogger(__name__) 49 | 50 | 51 | class DisplayWriterThread(Thread): 52 | 53 | def __init__( 54 | self, driver_cls: BaseDisplay.__class__, fix_type: ThreadSafeValue, 55 | lat: ThreadSafeValue, lon: ThreadSafeValue, 56 | extradata: ThreadSafeValue, fix_precision: ThreadSafeValue, 57 | should_clear: ThreadSafeValue, refresh_sec: int = 0 58 | ): 59 | super().__init__(name='DisplayWriter', daemon=True) 60 | self._fix_type: ThreadSafeValue = fix_type 61 | self._lat: ThreadSafeValue = lat 62 | self._lon: ThreadSafeValue = lon 63 | self._extradata: ThreadSafeValue = extradata 64 | self._fix_precision: ThreadSafeValue = fix_precision 65 | self._should_clear: ThreadSafeValue = should_clear 66 | self._driver_cls: BaseDisplay.__class__ = driver_cls 67 | self._refresh_sec = refresh_sec 68 | logger.info( 69 | 'Initialize DisplayWriterThread; driver_class=%s refresh_sec=%s', 70 | driver_cls, refresh_sec 71 | ) 72 | 73 | def run(self): 74 | logger.debug('Initialize display driver class') 75 | driver: BaseDisplay = self._driver_cls() 76 | if ( 77 | self._refresh_sec != 0 and 78 | driver.min_refresh_seconds > self._refresh_sec 79 | ): 80 | logger.debug( 81 | 'Set refresh_sec to %s based on driver\'s ' 82 | 'min_refresh_seconds=%s', self._refresh_sec, 83 | driver.min_refresh_seconds 84 | ) 85 | self._refresh_sec = driver.min_refresh_seconds 86 | logger.info('Refresh display every %d seconds', self._refresh_sec) 87 | while True: 88 | start = time.time() 89 | self.iteration(driver) 90 | duration = time.time() - start 91 | if duration < self._refresh_sec: 92 | t = self._refresh_sec - duration 93 | logger.debug('Sleep %s sec before next refresh', t) 94 | time.sleep(t) 95 | 96 | def iteration(self, driver: BaseDisplay): 97 | driver.update_display( 98 | fix_type=self._fix_type.get(), 99 | fix_precision=self._fix_precision.get(), 100 | lat=self._lat.get(), lon=self._lon.get(), 101 | extradata=self._extradata.get(), 102 | dt=datetime.now(timezone.utc), 103 | should_clear=self._should_clear.get() 104 | ) 105 | self._should_clear.set(False) 106 | 107 | 108 | class DisplayManager: 109 | 110 | def __init__(self, modname: str, clsname: str): 111 | self._fix_type: ThreadSafeValue = ThreadSafeValue(FixType.NO_GPS) 112 | self._fix_precision: ThreadSafeValue = ThreadSafeValue((0.0, 0.0)) 113 | self._lat: ThreadSafeValue = ThreadSafeValue() 114 | self._lon: ThreadSafeValue = ThreadSafeValue() 115 | self._extradata: ThreadSafeValue = ThreadSafeValue() 116 | self._should_clear: ThreadSafeValue = ThreadSafeValue(False) 117 | self._writer_thread: Optional[DisplayWriterThread] = None 118 | logger.debug('Import %s:%s', modname, clsname) 119 | mod = import_module(modname) 120 | self._driver_cls: BaseDisplay.__class__ = getattr(mod, clsname) 121 | self.clear() 122 | 123 | def start(self): 124 | refresh_sec = int(os.environ.get('DISPLAY_REFRESH_SEC', '0')) 125 | self._writer_thread = DisplayWriterThread( 126 | self._driver_cls, self._fix_type, self._lat, self._lon, 127 | self._extradata, self._fix_precision, self._should_clear, 128 | refresh_sec=refresh_sec 129 | ) 130 | self._writer_thread.start() 131 | 132 | def set_fix_type(self, gps_status: FixType): 133 | self._fix_type.set(gps_status) 134 | 135 | def set_fix_precision(self, precision: Tuple[float, float]): 136 | self._fix_precision.set(precision) 137 | 138 | def set_lat(self, lat: float): 139 | self._lat.set(lat) 140 | 141 | def set_lon(self, lon: float): 142 | self._lon.set(lon) 143 | 144 | def set_extradata(self, s: str): 145 | self._extradata.set(s) 146 | 147 | def clear(self): 148 | self._should_clear.set(True) 149 | -------------------------------------------------------------------------------- /pizero_gpslog/extradata/gq_gmc500plus.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | 37 | Note: for dependencies, this requires: 38 | 39 | pyudev==0.22.0 40 | 41 | """ 42 | 43 | import logging 44 | import os 45 | from glob import iglob 46 | from pyudev import Context, Devices 47 | from time import sleep, time 48 | from gmc import GMC 49 | from pizero_gpslog.extradata.base import BaseExtraDataProvider 50 | if not hasattr(GMC, 'get_config'): 51 | raise RuntimeError( 52 | 'ERROR: gmc must be installed from jantman\'s fork on the ' 53 | 'jantman-fixes-config branch; pip install ' 54 | 'git+https://gitlab.com/jantman/gmc.git@jantman-fixes-config' 55 | ) 56 | 57 | logger = logging.getLogger(__name__) 58 | 59 | 60 | class GqGMC500plus(BaseExtraDataProvider): 61 | 62 | _gmc_vendor_model_revision = [ 63 | ('1a86', '7523', '0263') 64 | ] 65 | 66 | def __init__(self, devname=None): 67 | super().__init__() 68 | self._original_devname = devname 69 | self._gmc = None 70 | self._data = self._default_response() 71 | self._sleep_time = int(os.environ.get('GMC_SLEEP_SEC', '5')) 72 | logger.info( 73 | 'Sleeping %d seconds between GMC polls; override by setting ' 74 | 'GMC_SLEEP_SEC environment variable as an int', self._sleep_time 75 | ) 76 | self._init_gmc() 77 | 78 | def _default_response(self): 79 | return { 80 | 'message': '', 81 | 'data': { 82 | 'time': time(), 83 | 'cps': None, 84 | 'cpsl': None, 85 | 'cpsh': None, 86 | 'cpm': None, 87 | 'cpml': None, 88 | 'cpmh': None, 89 | 'maxcps': None, 90 | 'calibration': None 91 | } 92 | } 93 | 94 | def _try_init(self): 95 | if self._original_devname is None: 96 | devname = self._find_usb_device() 97 | else: 98 | devname = self._original_devname 99 | if devname is None: 100 | logger.critical( 101 | 'ERROR: No devname given, and could not determine GMC-500+ ' 102 | 'device name using pyudev.' 103 | ) 104 | self._devname = devname 105 | return 106 | self._devname = devname 107 | logger.info('Using device: %s', devname) 108 | self._gmc = None 109 | logger.debug('Connecting to GMC...') 110 | self._gmc = GMC(config_update={'DEFAULT_PORT': self._devname}) 111 | logger.debug('Connected.') 112 | self._config = self._gmc.get_config() 113 | logger.info( 114 | 'GMC current time: %s; version: %s; serial: %s; voltage: %s; ' 115 | 'config: %s', self._gmc.get_date_time(), self._gmc.version(), 116 | self._gmc.serial(), self._gmc.voltage(), self._config 117 | ) 118 | calib_fields = [ 119 | 'CalibCPM_0', 'CalibuSv_0', 'CalibCPM_1', 'CalibuSv_1', 120 | 'CalibCPM_2', 'CalibuSv_2' 121 | ] 122 | self._calibration = { 123 | x: self._config[x] for x in calib_fields 124 | } 125 | 126 | def _init_gmc(self): 127 | self._gmc = None 128 | self._data = self._default_response() 129 | try: 130 | self._try_init() 131 | return 132 | except Exception as ex: 133 | logger.critical( 134 | 'Error initializing GMC; try again in 10s', ex 135 | ) 136 | logger.debug('GMC init error: %s', ex, exc_info=True) 137 | sleep(10) 138 | 139 | def run(self): 140 | logger.debug('Running extra data provider...') 141 | while True: 142 | try: 143 | cps = self._gmc.cps(numeric=True) 144 | cpsl = self._gmc.cpsl(numeric=True) 145 | cpsh = self._gmc.cpsh(numeric=True) 146 | cpm = self._gmc.cpm(numeric=True) 147 | cpml = self._gmc.cpml(numeric=True) 148 | cpmh = self._gmc.cpmh(numeric=True) 149 | maxcps = self._gmc.max_cps(numeric=True) 150 | logger.debug('End querying GMC') 151 | self._data = { 152 | 'message': f'{cps} CPS | {cpm} CPM', 153 | 'data': { 154 | 'time': time(), 155 | 'cps': cps, 156 | 'cpsl': cpsl, 157 | 'cpsh': cpsh, 158 | 'cpm': cpm, 159 | 'cpml': cpml, 160 | 'cpmh': cpmh, 161 | 'maxcps': maxcps, 162 | 'calibration': self._calibration 163 | } 164 | } 165 | except Exception as ex: 166 | logger.error( 167 | 'Error querying GMC; re-init. Error: %s', ex, exc_info=True 168 | ) 169 | self._data = self._default_response() 170 | self._init_gmc() 171 | sleep(self._sleep_time) 172 | 173 | def _find_usb_device(self): 174 | logger.debug('Using pyudev to find GMC tty device') 175 | context = Context() 176 | for devname in iglob('/dev/ttyUSB*'): 177 | device = Devices.from_device_file(context, devname) 178 | if device.properties['ID_BUS'] != 'usb': 179 | continue 180 | k = ( 181 | device.properties['ID_VENDOR_ID'], 182 | device.properties['ID_MODEL_ID'], 183 | device.properties['ID_REVISION'] 184 | ) 185 | if k in self._gmc_vendor_model_revision: 186 | logger.debug('Found GMC-500+ at: %s', devname) 187 | return devname 188 | return None 189 | 190 | def __del__(self): 191 | logger.info('Closing GMC device') 192 | self._gmc.close_device() 193 | -------------------------------------------------------------------------------- /pizero_gpslog/installer.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | import os 39 | import sys 40 | import logging 41 | import argparse 42 | from distutils.spawn import find_executable 43 | from textwrap import dedent 44 | from subprocess import run 45 | from pizero_gpslog.utils import set_log_debug 46 | 47 | FORMAT = "[%(asctime)s %(levelname)s] %(message)s" 48 | logging.basicConfig(level=logging.INFO, format=FORMAT) 49 | logger = logging.getLogger() 50 | 51 | 52 | class Installer(object): 53 | 54 | def __init__(self, args): 55 | self._systemctl = find_executable('systemctl') 56 | if self._systemctl is None: 57 | raise SystemExit( 58 | 'ERROR: Cannot find "systemctl" executable. This installer ' 59 | 'can only be used on systems with systemd running.' 60 | ) 61 | self.args = args 62 | 63 | def run(self): 64 | if self.args.dry_run: 65 | print(self.unit_file) 66 | return 67 | unitpath = '/etc/systemd/system/pizero-gpslog.service' 68 | if os.path.exists(unitpath): 69 | logger.warning( 70 | 'Unit file at %s already exists; replacing', unitpath 71 | ) 72 | logger.info('Writing systemd unit file:\n%s', self.unit_file) 73 | with open(unitpath, 'w') as fh: 74 | fh.write(self.unit_file) 75 | logger.info('systemd unit file written to: %s', unitpath) 76 | logger.info('Running "%s daemon-reload"' % self._systemctl) 77 | run([self._systemctl, 'daemon-reload'], check=True) 78 | logger.info('Running "%s enable pizero-gpslog"' % self._systemctl) 79 | run([self._systemctl, 'enable', 'pizero-gpslog'], check=True) 80 | logger.info('Installation complete. Service enabled.') 81 | 82 | @property 83 | def unit_file(self): 84 | return dedent(""" 85 | [Unit] 86 | Description=pizero-gpslog service 87 | Documentation=https://github.com/jantman/pizero-gpslog 88 | Requires=gpsd.service 89 | After=gpsd.service 90 | AssertArchitecture=arm 91 | [Service] 92 | Type=simple 93 | ExecStart={python} {fpath} 94 | WorkingDirectory={dirpath} 95 | User={user} 96 | Group={group} 97 | Environment=OUT_DIR={dirpath} FLUSH_FILE={flush} GPS_INTERVAL_SEC={intvl} LOG_LEVEL={log} {red} {green} 98 | RestartSec=10 99 | Restart=always 100 | [Install] 101 | WantedBy=default.target 102 | """.format( 103 | fpath=find_executable('pizero-gpslog'), 104 | python=sys.executable, 105 | user=self.args.user, 106 | group=self.args.group, 107 | dirpath=self.args.OUT_DIR, 108 | flush=('false' if self.args.FLUSH_FILE else 'true'), 109 | intvl=self.args.GPS_INTERVAL_SEC, 110 | log=self.args.LOG_LEVEL, 111 | red=( 112 | 'LED_PIN_RED=%s' % self.args.LED_PIN_RED 113 | if self.args.LED_PIN_RED is not None else '' 114 | ), 115 | green=( 116 | 'LED_PIN_GREEN=%s' % self.args.LED_PIN_GREEN 117 | if self.args.LED_PIN_GREEN is not None else '' 118 | ) 119 | )).strip() 120 | 121 | 122 | def parse_args(argv): 123 | """parse arguments/options""" 124 | p = argparse.ArgumentParser(description='pizero-gpslog installer - sets up ' 125 | 'systemd service') 126 | p.add_argument('-D', '--dry-run', dest='dry_run', action='store_true', 127 | default=False, 128 | help='Print generated systemd unit file to STDOUT and exit') 129 | p.add_argument('-v', '--verbose', dest='verbose', action='store_true', 130 | default=False, 131 | help='enable debug-level output.') 132 | p.add_argument( 133 | '-l', '--log-level', dest='LOG_LEVEL', action='store', type=str, 134 | choices=['WARNING', 'INFO', 'DEBUG'], default='WARNING', 135 | help='Log level to run daemon with; defaults to WARNING' 136 | ) 137 | p.add_argument( 138 | '-r', '--red-pin', dest='LED_PIN_RED', action='store', type=int, 139 | default=None, 140 | help='GPIO Pin number for Red (primary) LED; omit to use fake ' 141 | '(log to STDOUT) LEDs. Defaults to None (omitted).' 142 | ) 143 | p.add_argument( 144 | '-g', '--green-pin', dest='LED_PIN_GREEN', action='store', type=int, 145 | default=None, 146 | help='GPIO Pin number for Green (secondary) LED; omit to use fake ' 147 | '(log to STDOUT) LEDs. Defaults to None (omitted).' 148 | ) 149 | p.add_argument( 150 | '-i', '--interval', dest='GPS_INTERVAL_SEC', action='store', type=int, 151 | default=5, 152 | help='Interval in seconds to poll gpsd and write to file. Defaults ' 153 | 'to 5.' 154 | ) 155 | p.add_argument( 156 | '--no-flush', dest='FLUSH_FILE', action='store_true', default=False, 157 | help='Do not explicitly flush file after writing each record.' 158 | ) 159 | cwd = os.getcwd() 160 | p.add_argument( 161 | '-d', '--out-dir', dest='OUT_DIR', action='store', type=str, 162 | default=cwd, 163 | help='Directory to write output files in. Default is your current ' 164 | 'working directory (%s)' % cwd 165 | ) 166 | user = os.environ.get('SUDO_UID', '%d' % os.geteuid()) 167 | p.add_argument( 168 | '-u', '--user', dest='user', action='store', type=str, default=user, 169 | help='User to run daemon as (default: %s)' % user 170 | ) 171 | group = os.environ.get('SUDO_GID', '%d' % os.getegid()) 172 | p.add_argument( 173 | '-G', '--group', dest='group', action='store', type=str, default=group, 174 | help='Group to run daemon as (default: %s)' % group 175 | ) 176 | args = p.parse_args(argv) 177 | return args 178 | 179 | 180 | def main(): 181 | args = parse_args(sys.argv[1:]) 182 | 183 | # set logging level 184 | if args.verbose: 185 | set_log_debug(logger) 186 | 187 | Installer(args).run() 188 | 189 | 190 | if __name__ == "__main__": 191 | main() 192 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu303-climbing.log.chk: -------------------------------------------------------------------------------- 1 | $GPGSV,2,1,07,10,45,196,10,29,67,310,42,28,59,108,40,26,51,304,44*70 2 | $GPGSV,2,2,07,08,44,058,43,27,16,066,37,21,10,301,00*4A 3 | {"class":"SKY","time":"2005-06-19T16:12:22.890Z","xdop":1.02,"ydop":1.08,"vdop":2.56,"tdop":1.96,"hdop":1.48,"gdop":3.55,"pdop":2.96,"satellites":[{"PRN":10,"el":45,"az":196,"ss":10,"used":true},{"PRN":29,"el":67,"az":310,"ss":42,"used":true},{"PRN":28,"el":59,"az":108,"ss":40,"used":true},{"PRN":26,"el":51,"az":304,"ss":44,"used":true},{"PRN":8,"el":44,"az":58,"ss":43,"used":true},{"PRN":27,"el":16,"az":66,"ss":37,"used":true},{"PRN":21,"el":10,"az":301,"ss":0,"used":false}]} 4 | $GPZDA,161222.89,19,06,2005,00,00*6A 5 | $GPGGA,161222,4629.8923,N,00734.0837,E,1,06,3.20,1327.69,M,48.183,M,,*7A 6 | $GPRMC,161222,A,4629.8923,N,00734.0837,E,0.1673,180.000,190605,,*2D 7 | $GPGSA,A,3,10,29,28,26,8,27,,,,,,,3.0,3.2,2.6*0D 8 | $GPGBS,161222,15.28,M,16.17,M,58.85,M*05 9 | {"class":"TPV","mode":3,"time":"2005-06-19T16:12:22.890Z","ept":0.005,"lat":46.498204497,"lon":7.568061439,"alt":1327.689,"epx":15.279,"epy":16.167,"epv":58.845,"track":180.0000,"speed":0.086,"climb":-0.091} 10 | $GPGSV,2,1,07,10,45,196,08,29,67,310,41,28,59,108,40,26,51,304,43*7D 11 | $GPGSV,2,2,07,08,44,058,42,27,16,066,36,21,10,301,00*4A 12 | {"class":"SKY","time":"2005-06-19T16:12:23.890Z","xdop":1.02,"ydop":1.08,"vdop":2.56,"tdop":1.96,"hdop":3.20,"gdop":3.55,"pdop":2.96,"satellites":[{"PRN":10,"el":45,"az":196,"ss":8,"used":true},{"PRN":29,"el":67,"az":310,"ss":41,"used":true},{"PRN":28,"el":59,"az":108,"ss":40,"used":true},{"PRN":26,"el":51,"az":304,"ss":43,"used":true},{"PRN":8,"el":44,"az":58,"ss":42,"used":true},{"PRN":27,"el":16,"az":66,"ss":36,"used":true},{"PRN":21,"el":10,"az":301,"ss":0,"used":false}]} 13 | $GPZDA,161223.89,19,06,2005,00,00*6B 14 | $GPGGA,161223,4629.8923,N,00734.0837,E,1,06,3.20,1327.69,M,48.183,M,,*7B 15 | $GPRMC,161223,A,4629.8923,N,00734.0837,E,0.1776,10.380,190605,,*1B 16 | $GPGSA,A,3,10,29,28,26,8,27,,,,,,,3.0,3.2,2.6*0D 17 | {"class":"TPV","mode":3,"time":"2005-06-19T16:12:23.890Z","ept":0.005,"lat":46.498204497,"lon":7.568061439,"alt":1327.689,"epx":15.279,"epy":16.167,"epv":58.845,"track":10.3797,"speed":0.091,"climb":-0.085,"eps":32.33,"epc":117.69} 18 | $GPGSV,2,1,07,10,45,196,33,29,67,310,42,28,59,108,42,26,51,304,43*74 19 | $GPGSV,2,2,07,08,44,058,44,27,16,066,36,21,10,301,00*4C 20 | {"class":"SKY","time":"2005-06-19T16:12:24.890Z","xdop":1.02,"ydop":1.08,"vdop":2.56,"tdop":1.96,"hdop":3.20,"gdop":3.55,"pdop":2.96,"satellites":[{"PRN":10,"el":45,"az":196,"ss":33,"used":true},{"PRN":29,"el":67,"az":310,"ss":42,"used":true},{"PRN":28,"el":59,"az":108,"ss":42,"used":true},{"PRN":26,"el":51,"az":304,"ss":43,"used":true},{"PRN":8,"el":44,"az":58,"ss":44,"used":true},{"PRN":27,"el":16,"az":66,"ss":36,"used":true},{"PRN":21,"el":10,"az":301,"ss":0,"used":false}]} 21 | $GPZDA,161224.89,19,06,2005,00,00*6C 22 | $GPGGA,161224,4629.8923,N,00734.0837,E,1,06,1.40,1327.69,M,48.183,M,,*78 23 | $GPRMC,161224,A,4629.8923,N,00734.0837,E,0.1673,180.000,190605,,*2B 24 | $GPGSA,A,3,10,29,28,26,8,27,,,,,,,3.0,1.4,2.6*09 25 | {"class":"TPV","mode":3,"time":"2005-06-19T16:12:24.890Z","ept":0.005,"lat":46.498204497,"lon":7.568061439,"alt":1327.689,"epx":15.279,"epy":16.167,"epv":58.845,"track":180.0000,"speed":0.086,"climb":-0.091,"eps":32.33,"epc":117.69} 26 | $GPGSV,2,1,07,10,45,196,31,29,67,310,43,28,59,108,42,26,51,304,45*71 27 | $GPGSV,2,2,07,08,44,058,46,27,16,066,42,21,10,301,00*4D 28 | {"class":"SKY","time":"2005-06-19T16:12:25.890Z","xdop":1.02,"ydop":1.08,"vdop":2.56,"tdop":1.96,"hdop":1.40,"gdop":3.55,"pdop":2.96,"satellites":[{"PRN":10,"el":45,"az":196,"ss":31,"used":true},{"PRN":29,"el":67,"az":310,"ss":43,"used":true},{"PRN":28,"el":59,"az":108,"ss":42,"used":true},{"PRN":26,"el":51,"az":304,"ss":45,"used":true},{"PRN":8,"el":44,"az":58,"ss":46,"used":true},{"PRN":27,"el":16,"az":66,"ss":42,"used":true},{"PRN":21,"el":10,"az":301,"ss":0,"used":false}]} 29 | $GPZDA,161225.89,19,06,2005,00,00*6D 30 | $GPGGA,161225,4629.8923,N,00734.0837,E,1,06,1.40,1327.69,M,48.183,M,,*79 31 | $GPRMC,161225,A,4629.8923,N,00734.0837,E,0.0000,0.000,190605,,*20 32 | $GPGSA,A,3,10,29,28,26,8,27,,,,,,,3.0,1.4,2.6*09 33 | {"class":"TPV","mode":3,"time":"2005-06-19T16:12:25.890Z","ept":0.005,"lat":46.498204497,"lon":7.568061439,"alt":1327.689,"epx":15.279,"epy":16.167,"epv":58.845,"track":0.0000,"speed":0.000,"climb":0.000,"eps":32.33,"epc":117.69} 34 | $GPGSV,2,1,07,10,45,196,33,29,67,310,40,28,59,108,41,26,51,304,43*75 35 | $GPGSV,2,2,07,08,44,058,44,27,16,066,40,21,10,301,00*4D 36 | {"class":"SKY","time":"2005-06-19T16:12:26.890Z","xdop":1.02,"ydop":1.08,"vdop":2.56,"tdop":1.96,"hdop":1.40,"gdop":3.55,"pdop":2.96,"satellites":[{"PRN":10,"el":45,"az":196,"ss":33,"used":true},{"PRN":29,"el":67,"az":310,"ss":40,"used":true},{"PRN":28,"el":59,"az":108,"ss":41,"used":true},{"PRN":26,"el":51,"az":304,"ss":43,"used":true},{"PRN":8,"el":44,"az":58,"ss":44,"used":true},{"PRN":27,"el":16,"az":66,"ss":40,"used":true},{"PRN":21,"el":10,"az":301,"ss":0,"used":false}]} 37 | $GPZDA,161226.89,19,06,2005,00,00*6E 38 | $GPGGA,161226,4629.8919,N,00734.0837,E,1,06,1.40,1326.96,M,48.183,M,,*72 39 | $GPRMC,161226,A,4629.8919,N,00734.0837,E,0.1673,180.000,190605,,*20 40 | $GPGSA,A,3,10,29,28,26,8,27,,,,,,,3.0,1.4,2.6*09 41 | {"class":"TPV","mode":3,"time":"2005-06-19T16:12:26.890Z","ept":0.005,"lat":46.498198306,"lon":7.568061439,"alt":1326.964,"epx":15.279,"epy":16.167,"epv":58.845,"track":180.0000,"speed":0.086,"climb":-0.091,"eps":32.33,"epc":117.69} 42 | $GPGSV,2,1,07,10,45,196,34,29,67,310,40,28,59,108,43,26,51,304,43*70 43 | $GPGSV,2,2,07,08,44,058,42,27,16,066,39,21,10,301,00*45 44 | {"class":"SKY","time":"2005-06-19T16:12:27.890Z","xdop":1.02,"ydop":1.08,"vdop":2.56,"tdop":1.96,"hdop":1.40,"gdop":3.55,"pdop":2.96,"satellites":[{"PRN":10,"el":45,"az":196,"ss":34,"used":true},{"PRN":29,"el":67,"az":310,"ss":40,"used":true},{"PRN":28,"el":59,"az":108,"ss":43,"used":true},{"PRN":26,"el":51,"az":304,"ss":43,"used":true},{"PRN":8,"el":44,"az":58,"ss":42,"used":true},{"PRN":27,"el":16,"az":66,"ss":39,"used":true},{"PRN":21,"el":10,"az":301,"ss":0,"used":false}]} 45 | $GPZDA,161227.89,19,06,2005,00,00*6F 46 | $GPGGA,161227,4629.8919,N,00734.0837,E,1,06,1.40,1326.96,M,48.183,M,,*73 47 | $GPRMC,161227,A,4629.8919,N,00734.0837,E,0.0000,0.000,190605,,*2B 48 | $GPGSA,A,3,10,29,28,26,8,27,,,,,,,3.0,1.4,2.6*09 49 | {"class":"TPV","mode":3,"time":"2005-06-19T16:12:27.890Z","ept":0.005,"lat":46.498198306,"lon":7.568061439,"alt":1326.964,"epx":15.279,"epy":16.167,"epv":58.845,"track":0.0000,"speed":0.000,"climb":0.000,"eps":32.33,"epc":117.69} 50 | $GPGSV,2,1,07,10,45,196,35,29,67,310,39,28,59,108,43,26,51,304,43*7F 51 | $GPGSV,2,2,07,08,44,058,42,27,16,066,38,21,10,301,00*44 52 | {"class":"SKY","time":"2005-06-19T16:12:28.890Z","xdop":1.02,"ydop":1.08,"vdop":2.56,"tdop":1.96,"hdop":1.40,"gdop":3.55,"pdop":2.96,"satellites":[{"PRN":10,"el":45,"az":196,"ss":35,"used":true},{"PRN":29,"el":67,"az":310,"ss":39,"used":true},{"PRN":28,"el":59,"az":108,"ss":43,"used":true},{"PRN":26,"el":51,"az":304,"ss":43,"used":true},{"PRN":8,"el":44,"az":58,"ss":42,"used":true},{"PRN":27,"el":16,"az":66,"ss":38,"used":true},{"PRN":21,"el":10,"az":301,"ss":0,"used":false}]} 53 | $GPZDA,161228.89,19,06,2005,00,00*60 54 | $GPGGA,161228,4629.8919,N,00734.0837,E,1,06,1.40,1326.96,M,48.183,M,,*7C 55 | $GPRMC,161228,A,4629.8919,N,00734.0837,E,0.0000,0.000,190605,,*24 56 | $GPGSA,A,3,10,29,28,26,8,27,,,,,,,3.0,1.4,2.6*09 57 | {"class":"TPV","mode":3,"time":"2005-06-19T16:12:28.890Z","ept":0.005,"lat":46.498198306,"lon":7.568061439,"alt":1326.964,"epx":15.279,"epy":16.167,"epv":58.845,"track":0.0000,"speed":0.000,"climb":0.000,"eps":32.33,"epc":117.69} 58 | $GPGSV,2,1,07,10,45,196,37,29,67,310,40,28,59,108,45,26,51,304,42*74 59 | $GPGSV,2,2,07,08,44,058,42,27,16,066,38,21,10,301,00*44 60 | {"class":"SKY","time":"2005-06-19T16:12:29.890Z","xdop":1.02,"ydop":1.08,"vdop":2.56,"tdop":1.96,"hdop":1.40,"gdop":3.55,"pdop":2.96,"satellites":[{"PRN":10,"el":45,"az":196,"ss":37,"used":true},{"PRN":29,"el":67,"az":310,"ss":40,"used":true},{"PRN":28,"el":59,"az":108,"ss":45,"used":true},{"PRN":26,"el":51,"az":304,"ss":42,"used":true},{"PRN":8,"el":44,"az":58,"ss":42,"used":true},{"PRN":27,"el":16,"az":66,"ss":38,"used":true},{"PRN":21,"el":10,"az":301,"ss":0,"used":false}]} 61 | $GPZDA,161229.89,19,06,2005,00,00*61 62 | $GPGGA,161229,4629.8919,N,00734.0837,E,1,06,1.40,1326.96,M,48.183,M,,*7D 63 | $GPRMC,161229,A,4629.8919,N,00734.0837,E,0.0000,0.000,190605,,*25 64 | $GPGSA,A,3,10,29,28,26,8,27,,,,,,,3.0,1.4,2.6*09 65 | {"class":"TPV","mode":3,"time":"2005-06-19T16:12:29.890Z","ept":0.005,"lat":46.498198306,"lon":7.568061439,"alt":1326.964,"epx":15.279,"epy":16.167,"epv":58.845,"track":0.0000,"speed":0.000,"climb":0.000,"eps":32.33,"epc":117.69} 66 | -------------------------------------------------------------------------------- /pizero_gpslog/runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | import os 39 | import logging 40 | import time 41 | import json 42 | from typing import Optional 43 | from _io import TextIOWrapper 44 | from importlib import import_module 45 | 46 | from pizero_gpslog.gpsd import ( 47 | GpsClient, NoActiveGpsError, NoFixError, GpsResponse 48 | ) 49 | from pizero_gpslog.version import VERSION, PROJECT_URL 50 | from pizero_gpslog.utils import set_log_info, set_log_debug, FixType 51 | from pizero_gpslog.displaymanager import DisplayManager 52 | from pizero_gpslog.extradata.base import BaseExtraDataProvider 53 | 54 | if 'LED_PIN_RED' in os.environ and 'LED_PIN_GREEN' in os.environ: 55 | from gpiozero import LED 56 | else: 57 | from pizero_gpslog.fakeled import FakeLed as LED 58 | 59 | logger = logging.getLogger(__name__) 60 | 61 | 62 | class EmptyExtraData: 63 | 64 | def __init__(self): 65 | self.data = {'message': ''} 66 | 67 | def start(self): 68 | pass 69 | 70 | 71 | class GpsLogger(object): 72 | 73 | def __init__(self): 74 | logger.warning( 75 | 'Starting pizero-gpslog version %s <%s>', VERSION, PROJECT_URL 76 | ) 77 | led_1_pin: int = int(os.environ.get('LED_PIN_RED', '-1')) 78 | logger.info('Initializing LED1 (Red) on pin %d', led_1_pin) 79 | self.LED1: LED = LED(led_1_pin) 80 | led_2_pin: int = int(os.environ.get('LED_PIN_GREEN', '-2')) 81 | logger.info('Initializing LED2 (Green) on pin %d', led_2_pin) 82 | self.LED2: LED = LED(led_2_pin) 83 | self.LED2.on() 84 | logger.info('Connecting to gpsd') 85 | self.gps: GpsClient = GpsClient() 86 | self.interval_sec: int = int(os.environ.get('GPS_INTERVAL_SEC', '5')) 87 | logger.info('Sleeping %s seconds between writes', self.interval_sec) 88 | self.flush_file: str = os.environ.get('FLUSH_FILE', '') != 'false' 89 | self.outdir: str = os.path.abspath( 90 | os.environ.get('OUT_DIR', os.getcwd()) 91 | ) 92 | logger.debug('Writing logs in: %s', self.outdir) 93 | self._fh: Optional[TextIOWrapper] = None 94 | self._display: Optional[DisplayManager] = None 95 | if 'DISPLAY_CLASS' in os.environ: 96 | modname, clsname = os.environ['DISPLAY_CLASS'].split(':') 97 | self._display = DisplayManager(modname, clsname) 98 | self._display.set_fix_type(FixType.NO_GPS) 99 | self._display.start() 100 | if 'EXTRA_DATA_CLASS' in os.environ: 101 | modname, clsname = os.environ['EXTRA_DATA_CLASS'].split(':') 102 | logger.debug('Import %s:%s', modname, clsname) 103 | mod = import_module(modname) 104 | extra_cls: BaseExtraDataProvider.__class__ = getattr( 105 | mod, clsname 106 | ) 107 | self._extra_data_instance = extra_cls() 108 | self._extra_data_instance.start() 109 | else: 110 | self._extra_data_instance = EmptyExtraData() 111 | 112 | def run(self): 113 | self.LED2.off() 114 | while True: 115 | time.sleep(self.interval_sec) 116 | logger.debug('Reading current position from gpsd') 117 | try: 118 | packet = self.gps.current_fix 119 | except NoActiveGpsError: 120 | packet = GpsResponse() 121 | packet.mode = 0 122 | except NoFixError: 123 | packet = GpsResponse() 124 | packet.mode = 1 125 | self._handle_packet(packet) 126 | 127 | def _handle_waiting_gps(self, packet: GpsResponse): 128 | logger.warning( 129 | 'No data returned by gpsd (no active GPS) - %s', 130 | packet 131 | ) 132 | if not self.LED1.is_lit: 133 | self.LED1.on() 134 | if self._display is not None: 135 | self._display.set_fix_type(FixType.NO_GPS) 136 | self._display.set_extradata( 137 | self._extra_data_instance.data.get('message', '') 138 | ) 139 | 140 | def _handle_no_fix(self, packet: GpsResponse): 141 | logger.warning('No GPS fix yet - %s', packet) 142 | self.LED1.blink(on_time=0.1, off_time=0.1, n=3) 143 | if self._display is not None: 144 | self._display.set_fix_type(FixType.NO_FIX) 145 | self._display.set_extradata( 146 | self._extra_data_instance.data.get('message', '') 147 | ) 148 | 149 | def _ensure_file_open(self, packet: GpsResponse): 150 | if self._fh is not None: 151 | return 152 | logger.info( 153 | 'Got GPS packet with fix; GPS time is %s (UTC)' 154 | '' % packet.get_time() 155 | ) 156 | outfile = os.path.join( 157 | self.outdir, 158 | '%s.json' % packet.get_time().strftime('%Y-%m-%d_%H-%M-%S') 159 | ) 160 | logger.info('Writing output to: %s', outfile) 161 | self._fh = open(outfile, 'w', buffering=1) 162 | 163 | def _handle_fix(self, packet: GpsResponse): 164 | logger.info(packet) 165 | if packet.mode == 2: 166 | self.LED1.blink(on_time=0.5, off_time=0.25, n=2) 167 | if self._display is not None: 168 | self._display.set_fix_type(FixType.FIX_2D) 169 | elif packet.mode == 3: 170 | self.LED1.blink(on_time=0.5, off_time=0.25, n=1) 171 | if self._display is not None: 172 | self._display.set_fix_type(FixType.FIX_3D) 173 | if self._display is not None: 174 | self._display.set_fix_precision(packet.position_precision()) 175 | lat, lon = packet.position() 176 | self._display.set_lat(lat) 177 | self._display.set_lon(lon) 178 | self._display.set_extradata( 179 | self._extra_data_instance.data.get('message', '') 180 | ) 181 | 182 | def _handle_packet(self, packet: GpsResponse): 183 | if packet.mode == 0: 184 | return self._handle_waiting_gps(packet) 185 | if self.LED1.is_lit: 186 | self.LED1.off() 187 | if packet.mode == 1: 188 | return self._handle_no_fix(packet) 189 | # else we have a fix 190 | self._ensure_file_open(packet) 191 | if packet.mode in [2, 3]: 192 | self._handle_fix(packet) 193 | if self._extra_data_instance is not None: 194 | packet.raw_packet['_extra_data'] = self._extra_data_instance.data 195 | self._fh.write('%s\n' % json.dumps(packet.raw_packet)) 196 | if self.flush_file: 197 | self._fh.flush() 198 | self.LED2.blink(on_time=0.25, off_time=0.25, n=1) 199 | 200 | 201 | def main(): 202 | global logger 203 | format = "[%(asctime)s %(levelname)s] %(message)s" 204 | logging.basicConfig(level=logging.WARNING, format=format) 205 | logger = logging.getLogger() 206 | 207 | # set logging level 208 | if os.environ.get('LOG_LEVEL', None) == 'DEBUG': 209 | set_log_debug(logger) 210 | elif os.environ.get('LOG_LEVEL', None) == 'INFO': 211 | set_log_info(logger) 212 | GpsLogger().run() 213 | 214 | 215 | if __name__ == "__main__": 216 | main() 217 | -------------------------------------------------------------------------------- /pizero_gpslog/converter.py: -------------------------------------------------------------------------------- 1 | """ 2 | The latest version of this package is available at: 3 | 4 | 5 | ################################################################################## 6 | Copyright 2018-2020 Jason Antman 7 | 8 | This file is part of pizero-gpslog, also known as pizero-gpslog. 9 | 10 | pizero-gpslog is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | pizero-gpslog is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with pizero-gpslog. If not, see . 22 | 23 | The Copyright and Authors attributions contained herein may not be removed or 24 | otherwise altered, except to add the Author attribution of a contributor to 25 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 26 | ################################################################################## 27 | While not legally required, I sincerely request that anyone who finds 28 | bugs please submit them at or 29 | to me via email, and that you send any contributions or improvements 30 | either as a pull request on GitHub, or to me via email. 31 | ################################################################################## 32 | 33 | AUTHORS: 34 | Jason Antman 35 | ################################################################################## 36 | """ 37 | 38 | import sys 39 | import argparse 40 | import json 41 | 42 | import pint 43 | from gpxpy.gpx import GPX, GPXTrack, GPXTrackSegment, GPXTrackPoint 44 | from gpxpy.gpxfield import TIME_TYPE 45 | 46 | from pizero_gpslog.version import VERSION 47 | 48 | 49 | class GpxConverter(object): 50 | 51 | def __init__(self, input_fpath, imperial=False): 52 | self._in_fpath = input_fpath 53 | self._imperial = imperial 54 | self._ureg = pint.UnitRegistry() 55 | 56 | def convert(self): 57 | logs = [] 58 | with open(self._in_fpath, 'r', errors='ignore') as fh: 59 | lineno = 0 60 | for line in fh.readlines(): 61 | lineno += 1 62 | line = line.strip() 63 | if len(line) == 0: 64 | continue 65 | try: 66 | j = json.loads(line) 67 | except json.decoder.JSONDecodeError as ex: 68 | sys.stderr.write( 69 | 'Unable to decode JSON on line %s; skipping. ' 70 | '(ERROR: %s)\n' % ( 71 | lineno, ex 72 | ) 73 | ) 74 | continue 75 | if 'tpv' not in j: 76 | continue 77 | if j['tpv'][0].get('mode', 0) < 2: 78 | continue 79 | j['lineno'] = lineno 80 | logs.append(j) 81 | gpx = self._gpx_for_logs(logs) 82 | return gpx 83 | 84 | def stats_for_gpx(self, gpx): 85 | cloned_gpx = gpx.clone() 86 | cloned_gpx.reduce_points(2000, min_distance=10) 87 | cloned_gpx.smooth(vertical=True, horizontal=True) 88 | cloned_gpx.smooth(vertical=True, horizontal=False) 89 | moving_time, stopped_time, moving_distance, stopped_distance, \ 90 | max_speed_ms = cloned_gpx.get_moving_data() 91 | ud = gpx.get_uphill_downhill() 92 | elev = gpx.get_elevation_extremes() 93 | return { 94 | 'track_start': gpx.get_time_bounds().start_time, 95 | 'track_end': gpx.get_time_bounds().end_time, 96 | 'duration_sec': gpx.get_duration(), 97 | 'num_points': gpx.get_points_no(), 98 | 'moving_time': moving_time, 99 | 'stopped_time': stopped_time, 100 | 'moving_distance': moving_distance, 101 | 'stopped_distance': stopped_distance, 102 | 'max_speed_ms': max_speed_ms, 103 | '2d_horizontal_distance': gpx.length_2d(), 104 | 'total_elev_inc': ud.uphill, 105 | 'total_elev_dec': ud.downhill, 106 | 'min_elev': elev.minimum, 107 | 'max_elev': elev.maximum 108 | } 109 | 110 | def stats_text(self, stats): 111 | s = 'Track Start: %s UTC\n' % stats['track_start'] 112 | s += 'Track End: %s UTC\n' % stats['track_end'] 113 | s += 'Track Duration: %s\n' % seconds(stats['duration_sec']) 114 | s += '%d points in track\n' % stats['num_points'] 115 | s += 'Moving time: %s\n' % seconds(stats['moving_time']) 116 | s += 'Stopped time: %s\n' % seconds(stats['stopped_time']) 117 | s += 'Max Speed: %s\n' % self._ms_mph(stats['max_speed_ms']) 118 | s += '2D (Horizontal) distance: %s\n' % self._m_ftmi( 119 | stats['2d_horizontal_distance'] 120 | ) 121 | s += 'Total elevation increase: %s\n' % self._m_ft( 122 | stats['total_elev_inc'] 123 | ) 124 | s += 'Total elevation decrease: %s\n' % self._m_ft( 125 | stats['total_elev_dec'] 126 | ) 127 | s += 'Minimum elevation: %s\n' % self._m_ft(stats['min_elev']) 128 | s += 'Maximum elevation: %s\n' % self._m_ft(stats['max_elev']) 129 | return s 130 | 131 | def _gpx_for_logs(self, logs): 132 | g = GPX() 133 | track = GPXTrack() 134 | track.source = 'pizero-gpslog %s' % VERSION 135 | g.tracks.append(track) 136 | seg = GPXTrackSegment() 137 | track.segments.append(seg) 138 | prev_alt = 0.0 139 | 140 | for item in logs: 141 | try: 142 | tpv = item['tpv'][0] 143 | sky = item['sky'][0] 144 | alt = tpv.get( 145 | 'alt', item['gst'][0].get('alt', prev_alt) 146 | ) 147 | prev_alt = alt 148 | p = GPXTrackPoint( 149 | latitude=tpv['lat'], 150 | longitude=tpv['lon'], 151 | elevation=alt, 152 | time=TIME_TYPE.from_string(tpv['time']), 153 | speed=tpv['speed'], 154 | horizontal_dilution=sky.get('hdop', None), 155 | vertical_dilution=sky.get('vdop', None), 156 | position_dilution=sky.get('pdop', None) 157 | ) 158 | if tpv['mode'] == 2: 159 | p.type_of_gpx_fix = '2d' 160 | elif tpv['mode'] == 3: 161 | p.type_of_gpx_fix = '3d' 162 | if 'satellites' in sky: 163 | p.satellites = len(sky['satellites']) 164 | seg.points.append(p) 165 | except Exception: 166 | sys.stderr.write( 167 | 'Exception loading line %d:\n' % item['lineno'] 168 | ) 169 | raise 170 | return g 171 | 172 | def _ms_mph(self, n): 173 | if not self._imperial: 174 | return '%.4f m/s' % n 175 | val = n * self._ureg.meter / self._ureg.second 176 | return '%.4f MPH' % val.to(self._ureg.mile / self._ureg.hour).magnitude 177 | 178 | def _m_ftmi(self, n): 179 | if not self._imperial: 180 | return '%.4f m' % n 181 | val = n * self._ureg.meter 182 | val = val.to(self._ureg.mile) 183 | return '%.4f Mi' % val.magnitude 184 | 185 | def _m_ft(self, n): 186 | if not self._imperial: 187 | return '%.4f m' % n 188 | val = n * self._ureg.meter 189 | val = val.to(self._ureg.foot) 190 | return '%.4f ft' % val.magnitude 191 | 192 | 193 | def seconds(s): 194 | res = [] 195 | if s > 3600: 196 | h, s = divmod(s, 3600) 197 | res.append('%dh' % h) 198 | if s > 60: 199 | m, s = divmod(s, 60) 200 | res.append('%dm' % m) 201 | res.append('%ds' % s) 202 | return ' '.join(res) 203 | 204 | 205 | def main(argv=sys.argv[1:]): 206 | args = parse_args(argv) 207 | if args.output is None: 208 | if '.' not in args.JSON_FILE: 209 | args.output = args.JSON_FILE + '.' + args.format 210 | else: 211 | args.output = args.JSON_FILE.rsplit('.', 1)[0] + '.' + args.format 212 | conv = GpxConverter(args.JSON_FILE, imperial=args.imperial) 213 | gpx = conv.convert() 214 | with open(args.output, 'w') as fh: 215 | fh.write(gpx.to_xml()) 216 | sys.stderr.write('GPX file written to: %s' % args.output) 217 | if args.stats: 218 | print(conv.stats_text(conv.stats_for_gpx(gpx))) 219 | 220 | 221 | def parse_args(argv): 222 | """parse arguments/options""" 223 | p = argparse.ArgumentParser( 224 | description='Convert pizero-gpslog (gpsd POLL format) output files to ' 225 | 'common GPS formats.' 226 | ) 227 | p.add_argument('-f', '--format', dest='format', action='store', type=str, 228 | choices=['gpx'], default='gpx', 229 | help='destination format (default: gpx)') 230 | p.add_argument('-o', '--output', dest='output', action='store', type=str, 231 | default=None, 232 | help='Output file path. By default, will be the input ' 233 | 'file path with the file extension replaced with the ' 234 | 'correct one for the output format.') 235 | p.add_argument('-S', '--no-stats', dest='stats', action='store_false', 236 | default=True, 237 | help='do not print stats to STDERR' 238 | ) 239 | p.add_argument('-i', '--imperial', dest='imperial', action='store_true', 240 | default=False, help='output stats in imperial units') 241 | p.add_argument('JSON_FILE', action='store', type=str, 242 | help='Input file to convert') 243 | args = p.parse_args(argv) 244 | return args 245 | 246 | 247 | if __name__ == '__main__': 248 | main(sys.argv[1:]) 249 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu303-stillfix.log.chk: -------------------------------------------------------------------------------- 1 | $GPGSV,2,1,08,23,07,084,00,28,07,160,00,08,65,189,45,29,13,273,00*77 2 | $GPGSV,2,2,08,10,50,304,37,04,16,199,36,02,34,241,43,27,71,076,43*71 3 | {"class":"SKY","time":"2005-06-09T14:34:11.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.35,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":7,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":37,"used":true},{"PRN":4,"el":16,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":43,"used":true},{"PRN":27,"el":71,"az":76,"ss":43,"used":true}]} 4 | $GPZDA,143411.28,09,06,2005,00,00*66 5 | $GPGGA,143411,4629.8901,N,00734.0471,E,1,05,2.40,1349.51,M,48.183,M,,*75 6 | $GPRMC,143411,A,4629.8901,N,00734.0471,E,0.1776,10.379,090605,,*15 7 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 8 | $GPGBS,143411,25.19,M,24.69,M,80.26,M*06 9 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:11.280Z","ept":0.005,"lat":46.498167579,"lon":7.567452213,"alt":1349.507,"epx":25.195,"epy":24.691,"epv":80.261,"track":10.3789,"speed":0.091,"climb":-0.085} 10 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,43,29,13,273,00*70 11 | $GPGSV,2,2,08,10,50,304,36,04,16,199,36,02,34,241,44,27,71,076,43*77 12 | {"class":"SKY","time":"2005-06-09T14:34:12.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":43,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":36,"used":true},{"PRN":4,"el":16,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":44,"used":true},{"PRN":27,"el":71,"az":76,"ss":43,"used":true}]} 13 | $GPZDA,143412.28,09,06,2005,00,00*65 14 | $GPGGA,143412,4629.8905,N,00734.0473,E,1,05,2.40,1347.42,M,48.183,M,,*7C 15 | $GPRMC,143412,A,4629.8905,N,00734.0473,E,0.1776,10.379,090605,,*10 16 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 17 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:12.280Z","ept":0.005,"lat":46.498174322,"lon":7.567455643,"alt":1347.417,"epx":25.195,"epy":24.691,"epv":80.261,"track":10.3789,"speed":0.091,"climb":-0.085,"eps":50.39,"epc":160.52} 18 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,44,29,13,273,00*77 19 | $GPGSV,2,2,08,10,50,304,38,04,16,199,35,02,34,241,44,27,71,076,42*7B 20 | {"class":"SKY","time":"2005-06-09T14:34:13.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":44,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":38,"used":true},{"PRN":4,"el":16,"az":199,"ss":35,"used":true},{"PRN":2,"el":34,"az":241,"ss":44,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 21 | $GPZDA,143413.28,09,06,2005,00,00*64 22 | $GPGGA,143413,4629.8908,N,00734.0474,E,1,05,2.40,1346.73,M,48.183,M,,*74 23 | $GPRMC,143413,A,4629.8908,N,00734.0474,E,0.0000,0.000,090605,,*20 24 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 25 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:13.280Z","ept":0.005,"lat":46.498180789,"lon":7.567457358,"alt":1346.734,"epx":25.195,"epy":24.691,"epv":80.261,"track":0.0000,"speed":0.000,"climb":0.000,"eps":50.39,"epc":160.52} 26 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,45,29,13,273,00*76 27 | $GPGSV,2,2,08,10,50,304,38,04,16,199,35,02,34,241,43,27,71,076,43*7D 28 | {"class":"SKY","time":"2005-06-09T14:34:14.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":38,"used":true},{"PRN":4,"el":16,"az":199,"ss":35,"used":true},{"PRN":2,"el":34,"az":241,"ss":43,"used":true},{"PRN":27,"el":71,"az":76,"ss":43,"used":true}]} 29 | $GPZDA,143414.28,09,06,2005,00,00*63 30 | $GPGGA,143414,4629.8912,N,00734.0475,E,1,05,2.40,1346.05,M,48.183,M,,*78 31 | $GPRMC,143414,A,4629.8912,N,00734.0475,E,0.1776,10.379,090605,,*16 32 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 33 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:14.280Z","ept":0.005,"lat":46.498187256,"lon":7.567459073,"alt":1346.052,"epx":25.195,"epy":24.691,"epv":80.261,"track":10.3789,"speed":0.091,"climb":-0.085,"eps":50.39,"epc":160.52} 34 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,44,29,13,273,00*77 35 | $GPGSV,2,2,08,10,50,304,36,04,16,199,32,02,34,241,39,27,71,076,41*7B 36 | {"class":"SKY","time":"2005-06-09T14:34:15.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":44,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":36,"used":true},{"PRN":4,"el":16,"az":199,"ss":32,"used":true},{"PRN":2,"el":34,"az":241,"ss":39,"used":true},{"PRN":27,"el":71,"az":76,"ss":41,"used":true}]} 37 | $GPZDA,143415.28,09,06,2005,00,00*62 38 | $GPGGA,143415,4629.8909,N,00734.0475,E,1,05,2.40,1345.33,M,48.183,M,,*75 39 | $GPRMC,143415,A,4629.8909,N,00734.0475,E,0.1776,10.379,090605,,*1D 40 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 41 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:15.280Z","ept":0.005,"lat":46.498181065,"lon":7.567459073,"alt":1345.327,"epx":25.195,"epy":24.691,"epv":80.261,"track":10.3789,"speed":0.091,"climb":-0.085,"eps":50.39,"epc":160.52} 42 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,46,29,13,273,00*75 43 | $GPGSV,2,2,08,10,50,304,38,04,16,199,34,02,34,241,41,27,71,076,41*7C 44 | {"class":"SKY","time":"2005-06-09T14:34:16.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":46,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":38,"used":true},{"PRN":4,"el":16,"az":199,"ss":34,"used":true},{"PRN":2,"el":34,"az":241,"ss":41,"used":true},{"PRN":27,"el":71,"az":76,"ss":41,"used":true}]} 45 | $GPZDA,143416.28,09,06,2005,00,00*61 46 | $GPGGA,143416,4629.8913,N,00734.0476,E,1,05,2.40,1344.64,M,48.183,M,,*7D 47 | $GPRMC,143416,A,4629.8913,N,00734.0476,E,0.1673,180.000,090605,,*27 48 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 49 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:16.280Z","ept":0.005,"lat":46.498187532,"lon":7.567460788,"alt":1344.644,"epx":25.195,"epy":24.691,"epv":80.261,"track":180.0000,"speed":0.086,"climb":-0.091,"eps":50.39,"epc":160.52} 50 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,46,29,13,273,00*75 51 | $GPGSV,2,2,08,10,50,304,37,04,16,199,36,02,34,241,43,27,71,076,41*73 52 | {"class":"SKY","time":"2005-06-09T14:34:17.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":46,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":37,"used":true},{"PRN":4,"el":16,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":43,"used":true},{"PRN":27,"el":71,"az":76,"ss":41,"used":true}]} 53 | $GPZDA,143417.28,09,06,2005,00,00*60 54 | $GPGGA,143417,4629.8916,N,00734.0478,E,1,05,2.40,1343.96,M,48.183,M,,*7D 55 | $GPRMC,143417,A,4629.8916,N,00734.0478,E,0.1776,10.379,090605,,*1C 56 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 57 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:17.280Z","ept":0.005,"lat":46.498193999,"lon":7.567462504,"alt":1343.962,"epx":25.195,"epy":24.691,"epv":80.261,"track":10.3789,"speed":0.091,"climb":-0.085,"eps":50.39,"epc":160.52} 58 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,45,29,13,273,00*76 59 | $GPGSV,2,2,08,10,50,304,38,04,16,199,36,02,34,241,42,27,71,076,42*7E 60 | {"class":"SKY","time":"2005-06-09T14:34:18.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":38,"used":true},{"PRN":4,"el":16,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 61 | $GPZDA,143418.28,09,06,2005,00,00*6F 62 | $GPGGA,143418,4629.8916,N,00734.0478,E,1,05,2.40,1343.96,M,48.183,M,,*72 63 | $GPRMC,143418,A,4629.8916,N,00734.0478,E,0.0000,0.000,090605,,*28 64 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 65 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:18.280Z","ept":0.005,"lat":46.498193999,"lon":7.567462504,"alt":1343.962,"epx":25.195,"epy":24.691,"epv":80.261,"track":0.0000,"speed":0.000,"climb":0.000,"eps":50.39,"epc":160.52} 66 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,45,29,13,273,00*76 67 | $GPGSV,2,2,08,10,50,304,37,04,16,199,36,02,34,241,42,27,71,076,43*70 68 | {"class":"SKY","time":"2005-06-09T14:34:19.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":37,"used":true},{"PRN":4,"el":16,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":43,"used":true}]} 69 | $GPZDA,143419.28,09,06,2005,00,00*6E 70 | $GPGGA,143419,4629.8917,N,00734.0470,E,1,05,2.40,1343.87,M,48.183,M,,*7A 71 | $GPRMC,143419,A,4629.8917,N,00734.0470,E,0.0000,0.000,090605,,*20 72 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 73 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:19.280Z","ept":0.005,"lat":46.498194858,"lon":7.567449593,"alt":1343.871,"epx":25.195,"epy":24.691,"epv":80.261,"track":0.0000,"speed":0.000,"climb":0.000,"eps":50.39,"epc":160.52} 74 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,46,29,13,273,00*75 75 | $GPGSV,2,2,08,10,50,304,36,04,16,199,36,02,34,241,42,27,71,076,42*70 76 | {"class":"SKY","time":"2005-06-09T14:34:20.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":46,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":36,"used":true},{"PRN":4,"el":16,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 77 | $GPZDA,143420.28,09,06,2005,00,00*64 78 | $GPGGA,143420,4629.8921,N,00734.0471,E,1,05,2.40,1343.19,M,48.183,M,,*73 79 | $GPRMC,143420,A,4629.8921,N,00734.0471,E,0.1776,10.379,090605,,*15 80 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 81 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:20.280Z","ept":0.005,"lat":46.498201325,"lon":7.567451308,"alt":1343.189,"epx":25.195,"epy":24.691,"epv":80.261,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.39,"epc":160.52} 82 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,65,189,46,29,13,273,00*75 83 | $GPGSV,2,2,08,10,50,304,36,04,16,199,37,02,34,241,42,27,71,076,42*71 84 | {"class":"SKY","time":"2005-06-09T14:34:21.280Z","xdop":1.68,"ydop":1.65,"vdop":3.49,"tdop":3.10,"hdop":2.40,"gdop":5.23,"pdop":4.21,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":65,"az":189,"ss":46,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":50,"az":304,"ss":36,"used":true},{"PRN":4,"el":16,"az":199,"ss":37,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 85 | $GPZDA,143421.28,09,06,2005,00,00*65 86 | $GPGGA,143421,4629.8921,N,00734.0471,E,1,05,2.40,1343.19,M,48.183,M,,*72 87 | $GPRMC,143421,A,4629.8921,N,00734.0471,E,0.1776,10.379,090605,,*14 88 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.5*0E 89 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:21.280Z","ept":0.005,"lat":46.498201325,"lon":7.567451308,"alt":1343.189,"epx":25.195,"epy":24.691,"epv":80.261,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.39,"epc":160.52} 90 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu303-moving.log.chk: -------------------------------------------------------------------------------- 1 | $GPZDA,143443.28,09,06,2005,00,00*61 2 | $GPGGA,143443,4629.8972,N,00734.0447,E,1,00,2.40,1342.40,M,48.183,M,,*7D 3 | $GPRMC,143443,A,4629.8972,N,00734.0447,E,0.1776,10.379,090605,,*13 4 | $GPGSA,A,3,,,,,,,,,,,,,,2.4,*34 5 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:43.280Z","ept":0.005,"lat":46.498287178,"lon":7.567411672,"alt":1342.402,"track":10.3788,"speed":0.091,"climb":-0.085} 6 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,44,29,13,273,00*74 7 | $GPGSV,2,2,08,10,51,304,29,04,15,199,36,02,34,241,43,27,71,076,43*7C 8 | {"class":"SKY","time":"2005-06-09T14:34:44.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":44,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":29,"used":true},{"PRN":4,"el":15,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":43,"used":true},{"PRN":27,"el":71,"az":76,"ss":43,"used":true}]} 9 | $GPZDA,143444.28,09,06,2005,00,00*66 10 | $GPGGA,143444,4629.8976,N,00734.0447,E,1,05,2.40,1343.13,M,48.183,M,,*7C 11 | $GPRMC,143444,A,4629.8976,N,00734.0447,E,0.1776,10.379,090605,,*10 12 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 13 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:44.280Z","ept":0.005,"lat":46.498293369,"lon":7.567411672,"alt":1343.127,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 14 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,44,29,13,273,00*74 15 | $GPGSV,2,2,08,10,51,304,28,04,15,199,37,02,34,241,43,27,71,076,43*7C 16 | {"class":"SKY","time":"2005-06-09T14:34:45.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":44,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":28,"used":true},{"PRN":4,"el":15,"az":199,"ss":37,"used":true},{"PRN":2,"el":34,"az":241,"ss":43,"used":true},{"PRN":27,"el":71,"az":76,"ss":43,"used":true}]} 17 | $GPZDA,143445.28,09,06,2005,00,00*67 18 | $GPGGA,143445,4629.8980,N,00734.0440,E,1,05,2.40,1342.35,M,48.183,M,,*76 19 | $GPRMC,143445,A,4629.8980,N,00734.0440,E,0.1776,10.379,090605,,*1F 20 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 21 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:45.280Z","ept":0.005,"lat":46.498300695,"lon":7.567400477,"alt":1342.354,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 22 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,44,29,13,273,00*74 23 | $GPGSV,2,2,08,10,51,304,27,04,15,199,35,02,34,241,42,27,71,076,42*71 24 | {"class":"SKY","time":"2005-06-09T14:34:46.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":44,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":27,"used":true},{"PRN":4,"el":15,"az":199,"ss":35,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 25 | $GPZDA,143446.28,09,06,2005,00,00*64 26 | $GPGGA,143446,4629.8984,N,00734.0440,E,1,05,3.20,1343.08,M,48.183,M,,*79 27 | $GPRMC,143446,A,4629.8984,N,00734.0440,E,0.1776,10.379,090605,,*18 28 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,3.2,3.4*08 29 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:46.280Z","ept":0.005,"lat":46.498306887,"lon":7.567400477,"alt":1343.079,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 30 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,44,29,13,273,00*74 31 | $GPGSV,2,2,08,10,51,304,28,04,15,199,36,02,34,241,42,27,71,076,42*7D 32 | {"class":"SKY","time":"2005-06-09T14:34:47.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":3.20,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":44,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":28,"used":true},{"PRN":4,"el":15,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 33 | $GPZDA,143447.28,09,06,2005,00,00*65 34 | $GPGGA,143447,4629.8984,N,00734.0440,E,1,05,2.40,1343.08,M,48.183,M,,*7F 35 | $GPRMC,143447,A,4629.8984,N,00734.0440,E,0.1776,10.379,090605,,*19 36 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 37 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:47.280Z","ept":0.005,"lat":46.498306887,"lon":7.567400477,"alt":1343.079,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 38 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,45,29,13,273,00*75 39 | $GPGSV,2,2,08,10,51,304,28,04,15,199,38,02,34,241,43,27,71,076,42*72 40 | {"class":"SKY","time":"2005-06-09T14:34:48.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":28,"used":true},{"PRN":4,"el":15,"az":199,"ss":38,"used":true},{"PRN":2,"el":34,"az":241,"ss":43,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 41 | $GPZDA,143448.28,09,06,2005,00,00*6A 42 | $GPGGA,143448,4629.8992,N,00734.0441,E,1,05,2.40,1343.12,M,48.183,M,,*7D 43 | $GPRMC,143448,A,4629.8992,N,00734.0441,E,0.1776,10.379,090605,,*10 44 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 45 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:48.280Z","ept":0.005,"lat":46.498319545,"lon":7.567402192,"alt":1343.122,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 46 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,45,29,13,273,00*75 47 | $GPGSV,2,2,08,10,51,304,29,04,15,199,37,02,34,241,42,27,71,076,42*7D 48 | {"class":"SKY","time":"2005-06-09T14:34:49.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":29,"used":true},{"PRN":4,"el":15,"az":199,"ss":37,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 49 | $GPZDA,143449.28,09,06,2005,00,00*6B 50 | $GPGGA,143449,4629.8992,N,00734.0441,E,1,05,2.40,1343.12,M,48.183,M,,*7C 51 | $GPRMC,143449,A,4629.8992,N,00734.0441,E,0.1776,10.379,090605,,*11 52 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 53 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:49.280Z","ept":0.005,"lat":46.498319545,"lon":7.567402192,"alt":1343.122,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 54 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,45,29,13,273,00*75 55 | $GPGSV,2,2,08,10,51,304,32,04,15,199,36,02,34,241,43,27,71,076,42*77 56 | {"class":"SKY","time":"2005-06-09T14:34:50.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":32,"used":true},{"PRN":4,"el":15,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":43,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 57 | $GPZDA,143450.28,09,06,2005,00,00*63 58 | $GPGGA,143450,4629.8992,N,00734.0441,E,1,05,2.40,1343.12,M,48.183,M,,*74 59 | $GPRMC,143450,A,4629.8992,N,00734.0441,E,0.1776,10.379,090605,,*19 60 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 61 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:50.280Z","ept":0.005,"lat":46.498319545,"lon":7.567402192,"alt":1343.122,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 62 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,45,29,13,273,00*75 63 | $GPGSV,2,2,08,10,51,304,29,04,15,199,36,02,34,241,41,27,71,076,42*7F 64 | {"class":"SKY","time":"2005-06-09T14:34:51.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":29,"used":true},{"PRN":4,"el":15,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":41,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 65 | $GPZDA,143451.28,09,06,2005,00,00*62 66 | $GPGGA,143451,4629.8999,N,00734.0442,E,1,05,2.40,1343.17,M,48.183,M,,*78 67 | $GPRMC,143451,A,4629.8999,N,00734.0442,E,0.1776,10.379,090605,,*10 68 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 69 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:51.280Z","ept":0.005,"lat":46.498332203,"lon":7.567403907,"alt":1343.165,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 70 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,45,29,13,273,00*75 71 | $GPGSV,2,2,08,10,51,304,25,04,15,199,36,02,34,241,42,27,71,076,42*70 72 | {"class":"SKY","time":"2005-06-09T14:34:52.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":45,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":25,"used":true},{"PRN":4,"el":15,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 73 | $GPZDA,143452.28,09,06,2005,00,00*61 74 | $GPGGA,143452,4629.8999,N,00734.0442,E,1,05,3.20,1343.17,M,48.183,M,,*7C 75 | $GPRMC,143452,A,4629.8999,N,00734.0442,E,0.1776,10.379,090605,,*13 76 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,3.2,3.4*08 77 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:52.280Z","ept":0.005,"lat":46.498332203,"lon":7.567403907,"alt":1343.165,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3788,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 78 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,46,29,13,273,00*76 79 | $GPGSV,2,2,08,10,51,304,32,04,15,199,36,02,34,241,42,27,71,076,42*76 80 | {"class":"SKY","time":"2005-06-09T14:34:53.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":3.20,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":46,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":32,"used":true},{"PRN":4,"el":15,"az":199,"ss":36,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":42,"used":true}]} 81 | $GPZDA,143453.28,09,06,2005,00,00*60 82 | $GPGGA,143453,4629.9000,N,00734.0435,E,1,05,2.40,1343.07,M,48.183,M,,*73 83 | $GPRMC,143453,A,4629.9000,N,00734.0435,E,0.1776,10.379,090605,,*1A 84 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 85 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:53.280Z","ept":0.005,"lat":46.498333062,"lon":7.567390997,"alt":1343.075,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3787,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 86 | $GPGSV,2,1,08,23,06,084,00,28,07,160,00,08,66,189,46,29,13,273,00*76 87 | $GPGSV,2,2,08,10,51,304,31,04,15,199,37,02,34,241,42,27,71,076,43*75 88 | {"class":"SKY","time":"2005-06-09T14:34:54.280Z","xdop":1.66,"ydop":1.69,"vdop":3.42,"tdop":3.05,"hdop":2.40,"gdop":5.15,"pdop":4.16,"satellites":[{"PRN":23,"el":6,"az":84,"ss":0,"used":false},{"PRN":28,"el":7,"az":160,"ss":0,"used":false},{"PRN":8,"el":66,"az":189,"ss":46,"used":true},{"PRN":29,"el":13,"az":273,"ss":0,"used":false},{"PRN":10,"el":51,"az":304,"ss":31,"used":true},{"PRN":4,"el":15,"az":199,"ss":37,"used":true},{"PRN":2,"el":34,"az":241,"ss":42,"used":true},{"PRN":27,"el":71,"az":76,"ss":43,"used":true}]} 89 | $GPZDA,143454.28,09,06,2005,00,00*67 90 | $GPGGA,143454,4629.9004,N,00734.0436,E,1,05,2.40,1342.39,M,48.183,M,,*7F 91 | $GPRMC,143454,A,4629.9004,N,00734.0436,E,0.1776,10.379,090605,,*1A 92 | $GPGSA,A,3,8,10,4,2,27,,,,,,,,4.2,2.4,3.4*0F 93 | {"class":"TPV","mode":3,"time":"2005-06-09T14:34:54.280Z","ept":0.005,"lat":46.498339529,"lon":7.567392712,"alt":1342.392,"epx":24.829,"epy":25.326,"epv":78.615,"track":10.3787,"speed":0.091,"climb":-0.085,"eps":50.65,"epc":157.23} 94 | -------------------------------------------------------------------------------- /pizero_gpslog/gpsd.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file copied from `gpsd-py3 `_ 3 | by Martijn Braam, as of 41543d2 on October 14, 2017 (version 0.3.0). 4 | 5 | Licensed under the MIT license, per setup.py in the above repo. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | """ 24 | 25 | import socket 26 | import json 27 | import logging 28 | import datetime 29 | 30 | gpsTimeFormat = '%Y-%m-%dT%H:%M:%S.%fZ' 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | 35 | class NoFixError(Exception): 36 | pass 37 | 38 | 39 | class NoActiveGpsError(Exception): 40 | pass 41 | 42 | 43 | class GpsResponse(object): 44 | """ Class representing geo information returned by GPSD 45 | 46 | Use the attributes to get the raw gpsd data, use the methods to get parsed 47 | and corrected information. 48 | 49 | :type mode: int 50 | :type sats: int 51 | :type sats_valid: int 52 | :type lon: float 53 | :type lat: float 54 | :type alt: float 55 | :type track: float 56 | :type hspeed: float 57 | :type climb: float 58 | :type time: str 59 | :type error: dict[str, float] 60 | 61 | :var self.mode: Indicates the status of the GPS reception, 62 | 0=No value, 1=No fix, 2=2D fix, 3=3D fix 63 | :var self.sats: The number of satellites received by the GPS unit 64 | :var self.sats_valid: The number of satellites with valid information 65 | :var self.lon: Longitude in degrees 66 | :var self.lat: Latitude in degrees 67 | :var self.alt: Altitude in meters 68 | :var self.track: Course over ground, degrees from true north 69 | :var self.hspeed: Speed over ground, meters per second 70 | :var self.climb: Climb (positive) or sink (negative) rate, meters per second 71 | :var self.time: Time/date stamp in ISO8601 format, UTC. May have a 72 | fractional part of up to .001sec precision. 73 | :var self.error: GPSD error margin information 74 | 75 | GPSD error margin information 76 | ----------------------------- 77 | 78 | c: ecp: Climb/sink error estimate in meters/sec, 95% confidence. 79 | s: eps: Speed error estinmate in meters/sec, 95% confidence. 80 | t: ept: Estimated timestamp error (%f, seconds, 95% confidence). 81 | v: epv: Estimated vertical error in meters, 95% confidence. Present if mode 82 | is 3 and DOPs can be calculated from the satellite view. 83 | x: epx: Longitude error estimate in meters, 95% confidence. Present if mode 84 | is 2 or 3 and DOPs can be calculated from the satellite view. 85 | y: epy: Latitude error estimate in meters, 95% confidence. Present if mode 86 | is 2 or 3 and DOPs can be calculated from the satellite view. 87 | """ 88 | 89 | def __init__(self): 90 | self.mode = 0 91 | self.sats = 0 92 | self.sats_valid = 0 93 | self.lon = 0.0 94 | self.lat = 0.0 95 | self.alt = 0.0 96 | self.track = 0 97 | self.hspeed = 0 98 | self.climb = 0 99 | self.time = '' 100 | self.error = {} 101 | self._raw_response = {} 102 | 103 | @classmethod 104 | def from_json(cls, packet): 105 | """ Create GpsResponse instance based on the json data from GPSD 106 | :type packet: dict 107 | :param packet: JSON decoded GPSD response 108 | :return: GpsResponse 109 | """ 110 | result = cls() 111 | result._raw_response = packet 112 | if not packet['active']: 113 | raise NoActiveGpsError("No active GPS.") 114 | last_tpv = packet['tpv'][-1] 115 | last_sky = packet['sky'][-1] 116 | 117 | if 'satellites' in last_sky: 118 | result.sats = len(last_sky['satellites']) 119 | result.sats_valid = len( 120 | [sat for sat in last_sky['satellites'] if sat['used'] is True]) 121 | else: 122 | result.sats = 0 123 | result.sats_valid = 0 124 | 125 | result.mode = last_tpv['mode'] 126 | 127 | if last_tpv['mode'] >= 2: 128 | result.lon = last_tpv['lon'] if 'lon' in last_tpv else 0.0 129 | result.lat = last_tpv['lat'] if 'lat' in last_tpv else 0.0 130 | result.track = last_tpv['track'] if 'track' in last_tpv else 0 131 | result.hspeed = last_tpv['speed'] if 'speed' in last_tpv else 0 132 | result.time = last_tpv['time'] if 'time' in last_tpv else '' 133 | result.error = { 134 | 'c': 0, 135 | 's': last_tpv['eps'] if 'eps' in last_tpv else 0, 136 | 't': last_tpv['ept'] if 'ept' in last_tpv else 0, 137 | 'v': 0, 138 | 'x': last_tpv['epx'] if 'epx' in last_tpv else 0, 139 | 'y': last_tpv['epy'] if 'epy' in last_tpv else 0 140 | } 141 | 142 | if last_tpv['mode'] >= 3: 143 | result.alt = last_tpv['alt'] if 'alt' in last_tpv else 0.0 144 | result.climb = last_tpv['climb'] if 'climb' in last_tpv else 0 145 | result.error['c'] = last_tpv['epc'] if 'epc' in last_tpv else 0 146 | result.error['v'] = last_tpv['epv'] if 'epv' in last_tpv else 0 147 | 148 | return result 149 | 150 | def position(self): 151 | """ Get the latitude and longtitude as tuple. 152 | Needs at least 2D fix. 153 | 154 | :return: (float, float) 155 | """ 156 | if self.mode < 2: 157 | raise NoFixError("Needs at least 2D fix") 158 | return self.lat, self.lon 159 | 160 | def altitude(self): 161 | """ Get the altitude in meters. 162 | Needs 3D fix 163 | 164 | :return: (float) 165 | """ 166 | if self.mode < 3: 167 | raise NoFixError("Needs at least 3D fix") 168 | return self.alt 169 | 170 | def movement(self): 171 | """ Get the speed and direction of the current movement as dict 172 | 173 | The speed is the horizontal speed. 174 | The climb is the vertical speed 175 | The track is te direction of the motion 176 | Needs at least 3D fix 177 | 178 | :return: dict[str, float] 179 | """ 180 | if self.mode < 3: 181 | raise NoFixError("Needs at least 3D fix") 182 | return {"speed": self.hspeed, "track": self.track, "climb": self.climb} 183 | 184 | def speed_vertical(self): 185 | """ Get the vertical speed with the small movements filtered out. 186 | Needs at least 2D fix 187 | 188 | :return: float 189 | """ 190 | if self.mode < 2: 191 | raise NoFixError("Needs at least 2D fix") 192 | if abs(self.climb) < self.error['c']: 193 | return 0 194 | else: 195 | return self.climb 196 | 197 | def speed(self): 198 | """ Get the horizontal speed with the small movements filtered out. 199 | Needs at least 2D fix 200 | 201 | :return: float 202 | """ 203 | if self.mode < 2: 204 | raise NoFixError("Needs at least 2D fix") 205 | if self.hspeed < self.error['s']: 206 | return 0 207 | else: 208 | return self.hspeed 209 | 210 | def position_precision(self): 211 | """ Get the error margin in meters for the current fix. 212 | 213 | The first value return is the horizontal error, the second 214 | is the vertical error if a 3D fix is available 215 | 216 | Needs at least 2D fix 217 | 218 | :return: (float, float) 219 | """ 220 | if self.mode < 2: 221 | raise NoFixError("Needs at least 2D fix") 222 | return max(self.error['x'], self.error['y']), self.error['v'] 223 | 224 | def map_url(self): 225 | """ Get a openstreetmap url for the current position 226 | :return: str 227 | """ 228 | if self.mode < 2: 229 | raise NoFixError("Needs at least 2D fix") 230 | return "http://www.openstreetmap.org/?mlat={}&mlon={}&zoom=15".format( 231 | self.lat, self.lon 232 | ) 233 | 234 | def get_time(self, local_time=False): 235 | """ Get the GPS time 236 | 237 | :type local_time: bool 238 | :param local_time: Return date in the local timezone instead of UTC 239 | :return: datetime.datetime 240 | """ 241 | if self.mode < 2: 242 | raise NoFixError("Needs at least 2D fix") 243 | time = datetime.datetime.strptime(self.time, gpsTimeFormat) 244 | 245 | if local_time: 246 | time = time.replace(tzinfo=datetime.timezone.utc).astimezone() 247 | 248 | return time 249 | 250 | @property 251 | def raw_packet(self): 252 | """ 253 | Return the deserialized gpsd response, unaltered. 254 | 255 | :return: gpsd response, deserialized from JSON 256 | :rtype: dict 257 | """ 258 | return self._raw_response 259 | 260 | def __repr__(self): 261 | modes = { 262 | 0: 'No mode', 263 | 1: 'No fix', 264 | 2: '2D fix', 265 | 3: '3D fix' 266 | } 267 | if self.mode < 2: 268 | return "".format(modes[self.mode]) 269 | if self.mode == 2: 270 | return "".format(self.lat, self.lon) 271 | if self.mode == 3: 272 | return "".format( 273 | self.lat, self.lon, self.alt 274 | ) 275 | 276 | 277 | class GpsClient(object): 278 | 279 | def __init__(self, host="127.0.0.1", port=2947): 280 | """ Connect to a GPSD instance 281 | :param host: hostname for the GPSD server 282 | :param port: port for the GPSD server 283 | """ 284 | self._state = {} 285 | logger.debug("Connecting to gpsd socket at {}:{}".format(host, port)) 286 | self._gpsd_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 287 | self._gpsd_socket.connect((host, port)) 288 | self._gpsd_stream = self._gpsd_socket.makefile(mode="rw") 289 | logger.debug("Waiting for welcome message") 290 | welcome_raw = self._gpsd_stream.readline() 291 | welcome = json.loads(welcome_raw) 292 | if welcome['class'] != "VERSION": 293 | raise Exception( 294 | "Unexpected data received as welcome. Is the server a gpsd 3 " 295 | "server? (Data: %s)" % welcome_raw 296 | ) 297 | logger.debug("Enabling gps") 298 | self._gpsd_stream.write('?WATCH={"enable":true}\n') 299 | self._gpsd_stream.flush() 300 | 301 | for i in range(0, 2): 302 | raw = self._gpsd_stream.readline() 303 | parsed = json.loads(raw) 304 | self._parse_state_packet(parsed) 305 | 306 | def _parse_state_packet(self, json_data): 307 | if json_data['class'] == 'DEVICES': 308 | if not json_data['devices']: 309 | logger.warning('No gps devices found') 310 | self._state['devices'] = json_data 311 | elif json_data['class'] == 'WATCH': 312 | self._state['watch'] = json_data 313 | else: 314 | raise Exception( 315 | "Unexpected message received from gps: {}".format( 316 | json_data['class'] 317 | ) 318 | ) 319 | 320 | @property 321 | def current_fix(self): 322 | """ Poll gpsd for a new position 323 | :return: GpsResponse 324 | """ 325 | logger.debug("Polling gps") 326 | self._gpsd_stream.write("?POLL;\n") 327 | self._gpsd_stream.flush() 328 | raw = self._gpsd_stream.readline() 329 | response = json.loads(raw) 330 | if response['class'] != 'POLL': 331 | raise Exception( 332 | "Unexpected message received from gps: {}".format( 333 | response['class'] 334 | ) 335 | ) 336 | return GpsResponse.from_json(response) 337 | 338 | @property 339 | def device(self): 340 | """ Get information about current gps device 341 | :return: dict 342 | """ 343 | return { 344 | 'path': self._state['devices']['devices'][0]['path'], 345 | 'speed': self._state['devices']['devices'][0]['bps'], 346 | 'driver': self._state['devices']['devices'][0]['driver'] 347 | } 348 | -------------------------------------------------------------------------------- /pizero_gpslog/displays/epd2in13bc.py: -------------------------------------------------------------------------------- 1 | # ***************************************************************************** 2 | # * | File : epd2in13bc.py 3 | # * | Author : Waveshare team 4 | # * | Function : Electronic paper driver 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V4.0 8 | # * | Date : 2019-06-20 9 | # # | Info : python demo 10 | # ----------------------------------------------------------------------------- 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documnetation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | # 29 | """ 30 | Modified from: 31 | https://github.com/waveshare/e-Paper/blob/ 32 | 717cbb8d9215e58f9f3cdde45ee329f516504afe/RaspberryPi%26JetsonNano/python/ 33 | lib/waveshare_epd/epd2in13bc.py 34 | 35 | Display driver class for Waveshare e-Paper Display HAT 2.13 inch (B) 36 | 37 | https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT_(B) 38 | https://www.amazon.com/gp/product/B075FR81WL/ 39 | 40 | The latest version of this package is available at: 41 | 42 | 43 | ################################################################################## 44 | Copyright 2018-2020 Jason Antman 45 | 46 | This file is part of pizero-gpslog, also known as pizero-gpslog. 47 | 48 | pizero-gpslog is free software: you can redistribute it and/or modify 49 | it under the terms of the GNU Affero General Public License as published by 50 | the Free Software Foundation, either version 3 of the License, or 51 | (at your option) any later version. 52 | 53 | pizero-gpslog is distributed in the hope that it will be useful, 54 | but WITHOUT ANY WARRANTY; without even the implied warranty of 55 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 56 | GNU Affero General Public License for more details. 57 | 58 | You should have received a copy of the GNU Affero General Public License 59 | along with pizero-gpslog. If not, see . 60 | 61 | The Copyright and Authors attributions contained herein may not be removed or 62 | otherwise altered, except to add the Author attribution of a contributor to 63 | this work. (Additional Terms pursuant to Section 7b of the AGPL v3) 64 | ################################################################################## 65 | While not legally required, I sincerely request that anyone who finds 66 | bugs please submit them at or 67 | to me via email, and that you send any contributions or improvements 68 | either as a pull request on GitHub, or to me via email. 69 | ################################################################################## 70 | 71 | AUTHORS: 72 | Jason Antman 73 | ################################################################################## 74 | """ 75 | 76 | import time 77 | import logging 78 | import spidev 79 | import RPi.GPIO 80 | from typing import Optional, ClassVar, Tuple 81 | from pizero_gpslog.displays.base import BaseDisplay 82 | from pizero_gpslog.utils import FixType 83 | from datetime import datetime 84 | from PIL import Image, ImageDraw 85 | 86 | 87 | logger = logging.getLogger(__name__) 88 | 89 | 90 | class EPD2in13bc(BaseDisplay): 91 | 92 | #: width of the display in characters 93 | width_chars: ClassVar[int] = 21 94 | 95 | #: height of the display in lines 96 | height_lines: ClassVar[int] = 5 97 | 98 | #: the minimum number of seconds between refreshes of the display 99 | min_refresh_seconds: ClassVar[int] = 15 100 | 101 | def __init__( 102 | self, bus: int = 0, device: int = 0, rst_pin: int = 17, 103 | dc_pin: int = 25, cs_pin: int = 8, busy_pin: int = 24, 104 | epd_width: int = 104, epd_height: int = 212 105 | ): 106 | super().__init__() 107 | logger.debug( 108 | 'EPD.__init__(bus=%d, device=%d, rst_pin=%d, dc_pin=%d,' 109 | 'cs_pin=%d, busy_pin=%d, epd_width=%d, epd_height=%d)', 110 | bus, device, rst_pin, dc_pin, cs_pin, busy_pin, epd_width, 111 | epd_height 112 | ) 113 | self._GPIO = RPi.GPIO 114 | self._SPI = spidev.SpiDev(bus, device) 115 | self._reset_pin: int = rst_pin 116 | self._dc_pin: int = dc_pin 117 | self._busy_pin: int = busy_pin 118 | self._cs_pin: int = cs_pin 119 | self._width: int = epd_width 120 | self._height: int = epd_height 121 | self._GPIO.setmode(self._GPIO.BCM) 122 | self._GPIO.setwarnings(False) 123 | self._GPIO.setup(self._reset_pin, self._GPIO.OUT) 124 | self._GPIO.setup(self._dc_pin, self._GPIO.OUT) 125 | self._GPIO.setup(self._cs_pin, self._GPIO.OUT) 126 | self._GPIO.setup(self._busy_pin, self._GPIO.IN) 127 | self._SPI.max_speed_hz = 4000000 128 | self._SPI.mode = 0b00 129 | self._initialize() 130 | self._wrote_black: bool = True 131 | self._wrote_red: bool = True 132 | self.clear() 133 | self._wrote_black: bool = False 134 | self._wrote_red: bool = False 135 | time.sleep(1) # present in upstream example 136 | logger.debug('EPD initialize complete') 137 | 138 | def _digital_write(self, pin, value): 139 | self._GPIO.output(pin, value) 140 | 141 | def _digital_read(self, pin): 142 | return self._GPIO.input(pin) 143 | 144 | def _delay_ms(self, delaytime): 145 | time.sleep(delaytime / 1000.0) 146 | 147 | def _spi_writebyte(self, data): 148 | self._SPI.writebytes(data) 149 | 150 | def _hardware_reset(self): 151 | logger.debug('Reset EPD') 152 | self._digital_write(self._reset_pin, 1) 153 | self._delay_ms(200) 154 | self._digital_write(self._reset_pin, 0) 155 | self._delay_ms(10) 156 | self._digital_write(self._reset_pin, 1) 157 | self._delay_ms(200) 158 | 159 | def _send_command(self, command): 160 | self._digital_write(self._dc_pin, 0) 161 | self._digital_write(self._cs_pin, 0) 162 | self._spi_writebyte([command]) 163 | self._digital_write(self._cs_pin, 1) 164 | 165 | def _send_data(self, data): 166 | self._digital_write(self._dc_pin, 1) 167 | self._digital_write(self._cs_pin, 0) 168 | self._spi_writebyte([data]) 169 | self._digital_write(self._cs_pin, 1) 170 | 171 | @property 172 | def _is_busy(self): 173 | return self._digital_read(self._busy_pin) == 0 174 | 175 | def _wait_for_not_busy(self): 176 | logger.debug("Waiting until display is not busy (100ms check interval)") 177 | while self._is_busy: 178 | self._delay_ms(100) 179 | logger.debug("display is no longer busy") 180 | 181 | def _initialize(self): 182 | logger.debug('Initialize EPD') 183 | self._hardware_reset() 184 | self._send_command(0x06) # BOOSTER_SOFT_START 185 | self._send_data(0x17) 186 | self._send_data(0x17) 187 | self._send_data(0x17) 188 | self._send_command(0x04) # POWER_ON 189 | self._wait_for_not_busy() 190 | self._send_command(0x00) # PANEL_SETTING 191 | self._send_data(0x8F) 192 | self._send_command(0x50) # VCOM_AND_DATA_INTERVAL_SETTING 193 | self._send_data(0xF0) 194 | self._send_command(0x61) # RESOLUTION_SETTING 195 | self._send_data(self._width & 0xff) 196 | self._send_data(self._height >> 8) 197 | self._send_data(self._height & 0xff) 198 | logger.debug('EPD Initialized') 199 | 200 | def _getbuffer(self, image): 201 | buf = [0xFF] * (int(self._width/8) * self._height) 202 | image_monocolor = image.convert('1') 203 | imwidth, imheight = image_monocolor.size 204 | pixels = image_monocolor.load() 205 | if imwidth == self._width and imheight == self._height: 206 | logger.debug("Vertical") 207 | for y in range(imheight): 208 | for x in range(imwidth): 209 | # Set the bits for the column of pixels at the current position. 210 | if pixels[x, y] == 0: 211 | buf[int((x + y * self._width) / 8)] &= ~(0x80 >> (x % 8)) 212 | elif imwidth == self._height and imheight == self._width: 213 | logger.debug("Horizontal") 214 | for y in range(imheight): 215 | for x in range(imwidth): 216 | newx = y 217 | newy = self._height - x - 1 218 | if pixels[x, y] == 0: 219 | buf[int((newx + newy*self._width) / 8)] &= ~(0x80 >> (y % 8)) 220 | return buf 221 | 222 | def _display( 223 | self, black: Optional[Image.Image] = None, 224 | red: Optional[Image.Image] = None 225 | ): 226 | if black is not None: 227 | logger.debug('Displaying black image') 228 | buf = self._getbuffer(black) 229 | self._send_command(0x10) 230 | for i in range(0, int(self._width * self._height / 8)): 231 | self._send_data(buf[i]) 232 | self._send_command(0x92) 233 | self._wrote_black = True 234 | if red is not None: 235 | logger.debug('Displaying red image') 236 | buf = self._getbuffer(red) 237 | self._send_command(0x13) 238 | for i in range(0, int(self._width * self._height / 8)): 239 | self._send_data(buf[i]) 240 | self._send_command(0x92) 241 | self._wrote_red = True 242 | logger.debug('Refresh') 243 | self._send_command(0x12) # REFRESH 244 | self._wait_for_not_busy() 245 | logger.debug('Done refreshing') 246 | 247 | def update_display( 248 | self, fix_type: FixType, lat: float, lon: float, extradata: str, 249 | fix_precision: Tuple[float, float], dt: datetime, should_clear: bool 250 | ): 251 | if should_clear: 252 | self.clear() 253 | lines = [dt.strftime('%H:%M:%S UTC')] 254 | if fix_type == FixType.NO_GPS: 255 | lines.extend(['No GPS yet', '', '']) 256 | elif fix_type == FixType.NO_FIX: 257 | lines.extend(['No Fix yet', '', '']) 258 | else: 259 | ft = '??' 260 | if fix_type == FixType.FIX_2D: 261 | ft = '2D' 262 | elif fix_type == FixType.FIX_3D: 263 | ft = '3D' 264 | lines.append(f'{ft} {fix_precision[0]:.8},{fix_precision[1]:.8}') 265 | lines.append(f'Lat: {lat:.15}') 266 | lines.append(f'Lon: {lon:.15}') 267 | lines.append(extradata) 268 | self._write_lines(lines) 269 | 270 | def _write_lines(self, lines): 271 | """ 272 | Write ``lines`` to the display. 273 | """ 274 | logging.info('Begin update display') 275 | font = self.font(16) 276 | HBlackimage = Image.new('1', (self._height, self._width), 255) 277 | drawblack = ImageDraw.Draw(HBlackimage) 278 | for idx, content in enumerate(lines): 279 | drawblack.text( 280 | (0, 20 * idx), content, font=font, fill=0 281 | ) 282 | self._display(black=HBlackimage) 283 | logging.info('End update display') 284 | 285 | def clear(self): 286 | if self._wrote_black: 287 | logger.debug('Clearing black') 288 | self._send_command(0x10) 289 | for i in range(0, int(self._width * self._height / 8)): 290 | self._send_data(0xFF) 291 | self._send_command(0x92) 292 | self._wrote_black = False 293 | if self._wrote_red: 294 | logger.debug('Clearing red') 295 | self._send_command(0x13) 296 | for i in range(0, int(self._width * self._height / 8)): 297 | self._send_data(0xFF) 298 | self._send_command(0x92) 299 | self._wrote_red = False 300 | logger.debug('Done clearning') 301 | logger.debug('Refresh') 302 | self._send_command(0x12) # REFRESH 303 | self._wait_for_not_busy() 304 | logger.debug('Done refreshing') 305 | 306 | def _put_to_sleep(self): 307 | self._send_command(0x02) # POWER_OFF 308 | self._wait_for_not_busy() 309 | self._send_command(0x07) # DEEP_SLEEP 310 | self._send_data(0xA5) # check code 311 | 312 | def _destroy(self): 313 | logger.debug("spi end") 314 | self._SPI.close() 315 | logger.debug("close 5V, Module enters 0 power consumption ...") 316 | self._GPIO.output(self._reset_pin, 0) 317 | self._GPIO.output(self._dc_pin, 0) 318 | self._GPIO.cleanup() 319 | logger.debug('EPD cleaned up') 320 | 321 | def __del__(self): 322 | self._put_to_sleep() 323 | self._destroy() 324 | -------------------------------------------------------------------------------- /pizero_gpslog/tests/data/gpsd/bu353s4.log.chk: -------------------------------------------------------------------------------- 1 | $GPGGA,030719.000,3747.0873,S,17518.8938,E,1,08,1.1,59.9,M,23.7,M,,0000*7D 2 | {"class":"TPV","mode":3,"lat":-37.784788333,"lon":175.314896667,"alt":59.900} 3 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 4 | {"class":"TPV","mode":3,"lat":-37.784788333,"lon":175.314896667,"alt":59.900,"epv":34.500} 5 | $GPRMC,030719.000,A,3747.0873,S,17518.8938,E,0.69,181.39,311214,,,A*7D 6 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:19.000Z","ept":0.005,"lat":-37.784788333,"lon":175.314896667,"alt":59.900,"epv":34.500,"track":181.3900,"speed":0.355} 7 | $GPGGA,030720.000,3747.0875,S,17518.8938,E,1,08,1.1,59.8,M,23.7,M,,0000*70 8 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 9 | $GPRMC,030720.000,A,3747.0875,S,17518.8938,E,1.15,181.39,311214,,,A*7B 10 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:20.000Z","ept":0.005,"lat":-37.784791667,"lon":175.314896667,"alt":59.800,"epv":34.500,"track":181.3900,"speed":0.592,"climb":-0.100,"epc":69.00} 11 | $GPGGA,030721.000,3747.0876,S,17518.8934,E,1,08,1.1,60.1,M,23.7,M,,0000*7D 12 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 13 | $GPGSV,3,1,12,17,72,207,32,28,59,095,31,06,35,347,25,01,29,132,23*7B 14 | $GPGSV,3,2,12,30,23,011,29,20,17,069,20,26,09,293,23,13,07,304,10*7C 15 | $GPGSV,3,3,12,11,12,132,04,08,38,008,,02,35,348,,03,14,091,*73 16 | {"class":"SKY","xdop":0.63,"ydop":0.85,"vdop":1.50,"tdop":0.89,"hdop":1.10,"gdop":2.01,"pdop":1.80,"satellites":[{"PRN":17,"el":72,"az":207,"ss":32,"used":true},{"PRN":28,"el":59,"az":95,"ss":31,"used":true},{"PRN":6,"el":35,"az":347,"ss":25,"used":true},{"PRN":1,"el":29,"az":132,"ss":23,"used":true},{"PRN":30,"el":23,"az":11,"ss":29,"used":true},{"PRN":20,"el":17,"az":69,"ss":20,"used":true},{"PRN":26,"el":9,"az":293,"ss":23,"used":true},{"PRN":13,"el":7,"az":304,"ss":10,"used":true},{"PRN":11,"el":12,"az":132,"ss":4,"used":false},{"PRN":8,"el":38,"az":8,"ss":0,"used":false},{"PRN":2,"el":35,"az":348,"ss":0,"used":false},{"PRN":3,"el":14,"az":91,"ss":0,"used":false}]} 17 | $GPRMC,030721.000,A,3747.0876,S,17518.8934,E,0.89,181.39,311214,,,A*71 18 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:21.000Z","ept":0.005,"lat":-37.784793333,"lon":175.314890000,"alt":60.100,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.458,"climb":0.300,"epc":69.00} 19 | $GPGGA,030722.000,3747.0877,S,17518.8933,E,1,08,1.1,60.1,M,23.7,M,,0000*78 20 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 21 | $GPRMC,030722.000,A,3747.0877,S,17518.8933,E,0.12,181.39,311214,,,A*76 22 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:22.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314888333,"alt":60.100,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.062,"climb":0.000,"eps":25.58,"epc":69.00} 23 | $GPGGA,030723.000,3747.0878,S,17518.8935,E,1,08,1.1,60.0,M,23.7,M,,0000*71 24 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 25 | $GPRMC,030723.000,A,3747.0878,S,17518.8935,E,0.66,181.39,311214,,,A*7D 26 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:23.000Z","ept":0.005,"lat":-37.784796667,"lon":175.314891667,"alt":60.000,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.340,"climb":-0.100,"eps":25.58,"epc":69.00} 27 | $GPGGA,030724.000,3747.0878,S,17518.8936,E,1,08,1.1,60.1,M,23.7,M,,0000*74 28 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 29 | $GPRMC,030724.000,A,3747.0878,S,17518.8936,E,0.32,181.39,311214,,,A*78 30 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:24.000Z","ept":0.005,"lat":-37.784796667,"lon":175.314893333,"alt":60.100,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.165,"climb":0.100,"eps":25.58,"epc":69.00} 31 | $GPGGA,030725.000,3747.0878,S,17518.8936,E,1,08,1.1,60.2,M,23.7,M,,0000*76 32 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 33 | $GPRMC,030725.000,A,3747.0878,S,17518.8936,E,0.00,181.39,311214,,,A*78 34 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:25.000Z","ept":0.005,"lat":-37.784796667,"lon":175.314893333,"alt":60.200,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.000,"climb":0.100,"eps":25.58,"epc":69.00} 35 | $GPGGA,030726.000,3747.0877,S,17518.8936,E,1,08,1.1,60.6,M,23.7,M,,0000*7E 36 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 37 | $GPGSV,3,1,12,17,72,207,32,28,59,095,30,06,35,347,25,01,29,132,23*7A 38 | $GPGSV,3,2,12,30,23,011,29,20,17,069,20,26,09,293,23,13,07,304,10*7C 39 | $GPGSV,3,3,12,11,12,132,04,08,38,008,,02,35,348,,03,14,091,*73 40 | {"class":"SKY","xdop":0.63,"ydop":0.85,"vdop":1.50,"tdop":0.89,"hdop":1.10,"gdop":2.01,"pdop":1.80,"satellites":[{"PRN":17,"el":72,"az":207,"ss":32,"used":true},{"PRN":28,"el":59,"az":95,"ss":30,"used":true},{"PRN":6,"el":35,"az":347,"ss":25,"used":true},{"PRN":1,"el":29,"az":132,"ss":23,"used":true},{"PRN":30,"el":23,"az":11,"ss":29,"used":true},{"PRN":20,"el":17,"az":69,"ss":20,"used":true},{"PRN":26,"el":9,"az":293,"ss":23,"used":true},{"PRN":13,"el":7,"az":304,"ss":10,"used":true},{"PRN":11,"el":12,"az":132,"ss":4,"used":false},{"PRN":8,"el":38,"az":8,"ss":0,"used":false},{"PRN":2,"el":35,"az":348,"ss":0,"used":false},{"PRN":3,"el":14,"az":91,"ss":0,"used":false}]} 41 | $GPRMC,030726.000,A,3747.0877,S,17518.8936,E,0.59,181.39,311214,,,A*78 42 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:26.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314893333,"alt":60.600,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.304,"climb":0.400,"eps":25.58,"epc":69.00} 43 | $GPGGA,030727.000,3747.0877,S,17518.8938,E,1,08,1.1,60.8,M,23.7,M,,0000*7F 44 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 45 | $GPRMC,030727.000,A,3747.0877,S,17518.8938,E,0.29,181.39,311214,,,A*70 46 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:27.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314896667,"alt":60.800,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.149,"climb":0.200,"eps":25.58,"epc":69.00} 47 | $GPGGA,030728.000,3747.0878,S,17518.8940,E,1,08,1.1,60.5,M,23.7,M,,0000*7D 48 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 49 | $GPRMC,030728.000,A,3747.0878,S,17518.8940,E,0.52,181.39,311214,,,A*73 50 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:28.000Z","ept":0.005,"lat":-37.784796667,"lon":175.314900000,"alt":60.500,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.268,"climb":-0.300,"eps":25.58,"epc":69.00} 51 | $GPGGA,030729.000,3747.0879,S,17518.8942,E,1,08,1.1,60.7,M,23.7,M,,0000*7D 52 | $GPGSA,A,3,17,28,06,01,30,20,26,13,,,,,1.8,1.1,1.5*33 53 | $GPRMC,030729.000,A,3747.0879,S,17518.8942,E,0.48,181.39,311214,,,A*7A 54 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:29.000Z","ept":0.005,"lat":-37.784798333,"lon":175.314903333,"alt":60.700,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.247,"climb":0.200,"eps":25.58,"epc":69.00} 55 | $GPGGA,030730.000,3747.0878,S,17518.8942,E,1,07,1.1,60.5,M,23.7,M,,0000*79 56 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 57 | $GPRMC,030730.000,A,3747.0878,S,17518.8942,E,0.72,181.39,311214,,,A*7A 58 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:30.000Z","ept":0.005,"lat":-37.784796667,"lon":175.314903333,"alt":60.500,"epx":9.484,"epy":12.791,"epv":34.500,"track":181.3900,"speed":0.370,"climb":-0.200,"eps":25.58,"epc":69.00} 59 | $GPGGA,030731.000,3747.0877,S,17518.8941,E,1,07,1.1,60.5,M,23.7,M,,0000*74 60 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 61 | $GPGSV,3,1,12,17,71,207,32,28,59,095,30,06,36,347,25,01,29,132,23*7A 62 | $GPGSV,3,2,12,30,23,011,29,20,17,070,19,26,08,293,24,13,07,305,07*7F 63 | $GPGSV,3,3,12,08,38,008,,02,35,348,,03,15,091,,24,12,219,*7A 64 | {"class":"SKY","xdop":0.70,"ydop":0.85,"vdop":1.60,"tdop":0.99,"hdop":1.10,"gdop":2.14,"pdop":1.90,"satellites":[{"PRN":17,"el":71,"az":207,"ss":32,"used":true},{"PRN":28,"el":59,"az":95,"ss":30,"used":true},{"PRN":6,"el":36,"az":347,"ss":25,"used":true},{"PRN":1,"el":29,"az":132,"ss":23,"used":true},{"PRN":30,"el":23,"az":11,"ss":29,"used":true},{"PRN":20,"el":17,"az":70,"ss":19,"used":true},{"PRN":26,"el":8,"az":293,"ss":24,"used":true},{"PRN":13,"el":7,"az":305,"ss":7,"used":false},{"PRN":8,"el":38,"az":8,"ss":0,"used":false},{"PRN":2,"el":35,"az":348,"ss":0,"used":false},{"PRN":3,"el":15,"az":91,"ss":0,"used":false},{"PRN":24,"el":12,"az":219,"ss":0,"used":false}]} 65 | $GPRMC,030731.000,A,3747.0877,S,17518.8941,E,0.47,181.39,311214,,,A*71 66 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:31.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314901667,"alt":60.500,"epx":9.484,"epy":12.791,"epv":36.800,"track":181.3900,"speed":0.242,"climb":0.000,"eps":25.58,"epc":71.30} 67 | $GPGGA,030732.000,3747.0876,S,17518.8940,E,1,07,1.1,60.4,M,23.7,M,,0000*76 68 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 69 | $GPRMC,030732.000,A,3747.0876,S,17518.8940,E,0.43,181.39,311214,,,A*76 70 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:32.000Z","ept":0.005,"lat":-37.784793333,"lon":175.314900000,"alt":60.400,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.221,"climb":-0.100,"eps":25.59,"epc":73.60} 71 | $GPGGA,030733.000,3747.0876,S,17518.8941,E,1,07,1.1,60.2,M,23.7,M,,0000*70 72 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 73 | $GPRMC,030733.000,A,3747.0876,S,17518.8941,E,0.23,181.39,311214,,,A*70 74 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:33.000Z","ept":0.005,"lat":-37.784793333,"lon":175.314901667,"alt":60.200,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.118,"climb":-0.200,"eps":25.59,"epc":73.60} 75 | $GPGGA,030734.000,3747.0876,S,17518.8941,E,1,07,1.1,60.0,M,23.7,M,,0000*75 76 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 77 | $GPRMC,030734.000,A,3747.0876,S,17518.8941,E,0.51,181.39,311214,,,A*72 78 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:34.000Z","ept":0.005,"lat":-37.784793333,"lon":175.314901667,"alt":60.000,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.262,"climb":-0.200,"eps":25.59,"epc":73.60} 79 | $GPGGA,030735.000,3747.0876,S,17518.8941,E,1,07,1.1,60.0,M,23.7,M,,0000*74 80 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 81 | $GPRMC,030735.000,A,3747.0876,S,17518.8941,E,0.00,181.39,311214,,,A*77 82 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:35.000Z","ept":0.005,"lat":-37.784793333,"lon":175.314901667,"alt":60.000,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.000,"climb":0.000,"eps":25.59,"epc":73.60} 83 | $GPGGA,030736.000,3747.0876,S,17518.8941,E,1,07,1.1,60.0,M,23.7,M,,0000*77 84 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 85 | $GPGSV,3,1,12,17,71,207,32,28,59,095,30,06,36,347,25,01,29,132,23*7A 86 | $GPGSV,3,2,12,30,23,011,29,20,17,070,19,26,08,293,24,13,07,305,04*7C 87 | $GPGSV,3,3,12,08,38,008,,02,35,348,,03,15,091,,24,12,219,*7A 88 | {"class":"SKY","xdop":0.70,"ydop":0.85,"vdop":1.60,"tdop":0.99,"hdop":1.10,"gdop":2.14,"pdop":1.90,"satellites":[{"PRN":17,"el":71,"az":207,"ss":32,"used":true},{"PRN":28,"el":59,"az":95,"ss":30,"used":true},{"PRN":6,"el":36,"az":347,"ss":25,"used":true},{"PRN":1,"el":29,"az":132,"ss":23,"used":true},{"PRN":30,"el":23,"az":11,"ss":29,"used":true},{"PRN":20,"el":17,"az":70,"ss":19,"used":true},{"PRN":26,"el":8,"az":293,"ss":24,"used":true},{"PRN":13,"el":7,"az":305,"ss":4,"used":false},{"PRN":8,"el":38,"az":8,"ss":0,"used":false},{"PRN":2,"el":35,"az":348,"ss":0,"used":false},{"PRN":3,"el":15,"az":91,"ss":0,"used":false},{"PRN":24,"el":12,"az":219,"ss":0,"used":false}]} 89 | $GPRMC,030736.000,A,3747.0876,S,17518.8941,E,0.00,181.39,311214,,,A*74 90 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:36.000Z","ept":0.005,"lat":-37.784793333,"lon":175.314901667,"alt":60.000,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.000,"climb":0.000,"eps":25.59,"epc":73.60} 91 | $GPGGA,030737.000,3747.0877,S,17518.8942,E,1,07,1.1,60.0,M,23.7,M,,0000*74 92 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 93 | $GPRMC,030737.000,A,3747.0877,S,17518.8942,E,0.25,181.39,311214,,,A*70 94 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:37.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314903333,"alt":60.000,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.129,"climb":0.000,"eps":25.59,"epc":73.60} 95 | $GPGGA,030738.000,3747.0877,S,17518.8942,E,1,07,1.1,60.0,M,23.7,M,,0000*7B 96 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 97 | $GPRMC,030738.000,A,3747.0877,S,17518.8942,E,0.00,181.39,311214,,,A*78 98 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:38.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314903333,"alt":60.000,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.000,"climb":0.000,"eps":25.59,"epc":73.60} 99 | $GPGGA,030739.000,3747.0877,S,17518.8942,E,1,07,1.1,60.0,M,23.7,M,,0000*7A 100 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 101 | $GPRMC,030739.000,A,3747.0877,S,17518.8942,E,0.00,181.39,311214,,,A*79 102 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:39.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314903333,"alt":60.000,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.000,"climb":0.000,"eps":25.59,"epc":73.60} 103 | $GPGGA,030740.000,3747.0877,S,17518.8942,E,1,07,1.1,60.2,M,23.7,M,,0000*76 104 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 105 | $GPRMC,030740.000,A,3747.0877,S,17518.8942,E,0.26,181.39,311214,,,A*73 106 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:40.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314903333,"alt":60.200,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.134,"climb":0.200,"eps":25.59,"epc":73.60} 107 | $GPGGA,030741.000,3747.0877,S,17518.8941,E,1,07,1.1,60.2,M,23.7,M,,0000*74 108 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 109 | $GPGSV,3,1,12,17,71,207,31,28,59,095,30,06,36,347,26,01,29,132,23*7A 110 | $GPGSV,3,2,12,30,23,011,29,20,17,070,18,26,08,293,24,13,07,305,04*7D 111 | $GPGSV,3,3,12,08,38,008,,02,35,348,,03,15,091,,24,12,219,*7A 112 | {"class":"SKY","xdop":0.70,"ydop":0.85,"vdop":1.60,"tdop":0.99,"hdop":1.10,"gdop":2.14,"pdop":1.90,"satellites":[{"PRN":17,"el":71,"az":207,"ss":31,"used":true},{"PRN":28,"el":59,"az":95,"ss":30,"used":true},{"PRN":6,"el":36,"az":347,"ss":26,"used":true},{"PRN":1,"el":29,"az":132,"ss":23,"used":true},{"PRN":30,"el":23,"az":11,"ss":29,"used":true},{"PRN":20,"el":17,"az":70,"ss":18,"used":true},{"PRN":26,"el":8,"az":293,"ss":24,"used":true},{"PRN":13,"el":7,"az":305,"ss":4,"used":false},{"PRN":8,"el":38,"az":8,"ss":0,"used":false},{"PRN":2,"el":35,"az":348,"ss":0,"used":false},{"PRN":3,"el":15,"az":91,"ss":0,"used":false},{"PRN":24,"el":12,"az":219,"ss":0,"used":false}]} 113 | $GPRMC,030741.000,A,3747.0877,S,17518.8941,E,0.00,181.39,311214,,,A*75 114 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:41.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314901667,"alt":60.200,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.000,"climb":0.000,"eps":25.59,"epc":73.60} 115 | $GPGGA,030742.000,3747.0877,S,17518.8941,E,1,07,1.1,60.2,M,23.7,M,,0000*77 116 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 117 | $GPRMC,030742.000,A,3747.0877,S,17518.8941,E,0.00,181.39,311214,,,A*76 118 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:42.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314901667,"alt":60.200,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.000,"climb":0.000,"eps":25.59,"epc":73.60} 119 | $GPGGA,030743.000,3747.0877,S,17518.8941,E,1,07,1.1,60.2,M,23.7,M,,0000*76 120 | $GPGSA,A,3,17,28,06,01,30,20,26,,,,,,1.9,1.1,1.6*33 121 | $GPRMC,030743.000,A,3747.0877,S,17518.8941,E,0.00,181.39,311214,,,A*77 122 | {"class":"TPV","mode":3,"time":"2014-12-31T03:07:43.000Z","ept":0.005,"lat":-37.784795000,"lon":175.314901667,"alt":60.200,"epx":10.467,"epy":12.797,"epv":36.800,"track":181.3900,"speed":0.000,"climb":0.000,"eps":25.59,"epc":73.60} 123 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ABANDONED PROJECT - pizero-gpslog 2 | ================================= 3 | 4 | .. image:: https://www.repostatus.org/badges/latest/unsupported.svg 5 | :alt: Project Status: Unsupported – The project has reached a stable, usable state but the author(s) have ceased all work on it. A new maintainer may be desired. 6 | :target: https://www.repostatus.org/#unsupported 7 | 8 | .. image:: https://img.shields.io/pypi/v/pizero-gpslog.svg 9 | :alt: PyPI version badge 10 | :target: https://pypi.org/project/pizero-gpslog/ 11 | 12 | Raspberry Pi Zero gpsd logger with status LEDs. 13 | 14 | .. image:: http://blog.jasonantman.com/GFX/pizero_gpslogger_1_sm.jpg 15 | :alt: Photograph of finished hardware next to playing card deck for size comparison 16 | :target: http://blog.jasonantman.com/GFX/pizero_gpslogger_1.jpg 17 | 18 | A longer description of the motivation behind this and the specific hardware that I used is available on my blog: `DIY Raspberry Pi Zero GPS Track Logger `_. 19 | 20 | Introduction and Goals 21 | ---------------------- 22 | 23 | This is a trivial (and not really "supported") project of mine to couple a Raspberry Pi Zero with a USB GPS receiver and a battery pack to GPS track my hikes. The hardware decision was mainly based on what I had lying around: a Raspberry Pi Zero, a 10,000mAh external battery pack for my cell phone, and a USB GPS (well, I thought I had one, and got far enough into the project when I decided it was missing for good that I bought another). 24 | 25 | The goal is to package all of this together into a "small" (but not necessarily lightweight, based on the components) package that I can put in the outside pocket of my hiking pack, and record an accurate and detailed GPS track of my hikes. It writes the most recent position data from gpsd to disk at a user-defined interval, flushes IO after each write (so that it's safe to just pull the power on the Pi), and uses two LEDs to indicate status while in the field. Data is written in gpsd's native format, but a conversion tool is provided. 26 | 27 | This program relies on `gpsd `_ to interact with the GPS itself, as it's very mature and stable software, exposes a simple JSON-based socket interface, and also has decent Python bindings. There's no reason for a logger to have to worry about the nuances of GPS communication itself. 28 | 29 | Requirements 30 | ------------ 31 | 32 | * Raspberry Pi (tested with `Pi Zero `_ 1.3) and a MicroSD card (I'm using an `8GB SanDisk Ultra Class 10 UHS-1 `_, which has enough space after the OS for 240 days of 5-second-interval data). 33 | * Raspberry Pi OS with Python3 (see installation instructions below) 34 | * `gpsd compatible `_ GPS (I use a `GlobalSat BU-353-S4 USB `_; the gpsd folks say some pretty awful things about it, but we'll see...) 35 | * Recommended, one of: 36 | 37 | * Two GPIO-connected LEDs on the RPi, ideally different colors (see below). 38 | * A bitmap display, such as the `Waveshare 2.13 inch E-Ink Display Hat (B) `__, which I got `on Amazon `__ for $25 USD. While e-Ink displays are comparatively sluggish (this one takes an astonishing 15 seconds to re-draw the screen), they offer some major advantages for this purpose: they have very low power consumption, and the displayed information stays visible until the next refresh even without power. This means that if you have a GPS display that refreshes every minute, it will still show the last coordinates as of when it lost power. The one I purchased is also fully assembled and just connects directly to the Pi's 40-pin header. 39 | * An `adafruit 4567 2.23" Monochrome OLED Bonnet `_ OLED. This is much brighter than the e-Ink and can refresh at up to 30Hz, but draws more power. Good if you're using this in your vehicle and have "unlimited" power. 40 | * Some other variety of display, if you're willing to write a driver class for it. See below for further information. 41 | 42 | * Some sort of power source for the Pi. I use a standard adapter when testing and a 10000mAh USB battery pack in the field (specifically the `Anker PowerCore Speed 10000 QA `_). That battery pack is extreme overkill, and powers the unit continuously for 42 hours, when logging at a 5-second interval. 43 | 44 | Software Installation 45 | --------------------- 46 | 47 | This assumes you're running on Linux... 48 | 49 | 1. Download the latest `Raspberry Pi OS Lite `_ image. I'm using the "May 2020" version, released 2020-05-27, kernel 4.19. 50 | 2. Verify the checksum on the file and then unzip it. 51 | 3. Put the MicroSD card in your machine and write the image to it. As root, ``dd bs=4M if=2020-05-27-raspios-buster-lite-armhf.img of=/dev/sdX conv=fsync status=progress`` (where ``/dev/sdX`` is the device that the SD card showed up at). 52 | 4. Wait for the copy to finish and IO to the device to stop (run ``sync``). 53 | 5. *Optional:* If you're going to be using this on a network (i.e. for setup, troubleshooting, development, etc.) then this would be a good time to mount the Raspian partition on your computer and make some changes. See `setup_pi.sh `_ for an example. 54 | 55 | 1. Copy your authorized_keys file over to ``/home/pi``. 56 | 2. Touch the file at ``/boot/ssh`` on the Pi boot partition to enable SSH access. 57 | 3. Set a hostname. 58 | 4. If desired, configure WiFi (as well as downloading the packages for any required drivers onto the OS partition). 59 | 5. When finished, unmount, ``sync`` and remove the SD card. 60 | 61 | 6. Put the SD card in your Pi and plug it in. If you're going to be connecting directly with a keyboard and monitor, do so. If you configured WiFi (or want to use a USB Ethernet adapter) and have everything setup correctly, it should eventually connect to your network. If you have issues with a USB Ethernet adapter, try letting the Pi boot up (give it 2-3 minutes) and *then* plug in the adapter. 62 | 7. Log in. The default user is named "pi" with a default password of "raspberry". Run sudo `raspi-config `_ to set things like the locale and timezone. Personally, I usually leave the ``pi`` user password at its default for devices that will never be on an untrusted network. If using a SPI display, enable the SPI kernel module via ``raspi-config``. If you're using an I2C display, enable the I2C module. Reboot as needed. 63 | 8. ``sudo apt-get update && sudo apt-get install haveged git python3-gpiozero python3-setuptools python3-pip gpsd python-gps python3-pillow`` 64 | 9. If using a display such as the one recommended: ``sudo apt-get install python3-numpy && sudo pip3 install RPi.GPIO`` 65 | 10. Run ``sudo pip3 install pizero-gpslog && sudo pizero-gpslog-install``. The installer, ``pizero-gpslog-install``, templates out a systemd unit file, reloads systemd, and enables the unit. Environment variables to set for the service are taken from command line arguments; see ``pizero-gpslog-install --help`` for details. They can be changed after install by editing ``/etc/systemd/system/pizero-gpslog.service`` 66 | 11. Find out the USB vendor and product IDs for your GPS. My BU-353S4 uses a Prolific PL2303 serial chipset (vendor 067b product 2303) which is disabled by default in the Debian gpsd udev rules because it matches too many devices. Look at ``/lib/udev/rules.d/60-gpsd.rules``. If your GPS is commented out like mine, uncomment it and save the file. 67 | 12. If you're ready, ``sudo systemctl start pizero-gpslog`` to start it. Otherwise, it will start on the next boot. 68 | 69 | Hardware Installation & Setup 70 | ----------------------------- 71 | 72 | GPS 73 | +++ 74 | 75 | This should be pretty simple. Plug your GPS into the USB input on the RPi, via a "usb on the go" (USB OTG) cable. 76 | 77 | LED Indicators for GPS Fix & Disk Write 78 | +++++++++++++++++++++++++++++++++++++++ 79 | 80 | The simplest status indication adds two LEDs to the Pi Zero. I prefer to solder a female right-angle header to the Pi, then put the LEDs on a male header so they can be removed. gpiozero, the library used for controlling the LEDs, has `pinout diagrams `_ and information on the `wiring that the API expects `_. The code this project uses expects the LEDs to be wired active-high (cathode to ground, anode to GPIO pin through a limiting resistor). I made up a small 8x20 header for my LEDs and (very sloppily) potted them in epoxy. 81 | 82 | The LEDs are configured using the ``LED_PIN_RED`` and ``LED_PIN_GREEN`` environment variables, as described in the Configuration section. 83 | 84 | The LED outputs are as follows: 85 | 86 | * Green Solid (at start) - connecting to gpsd. Green LED goes out when connected to gpsd and the output file is opened for writing. 87 | * Red Solid - no active GPS (gpsd does not yet have an active gps, or no GPS is connected). 88 | * Red 3 Fast Blinks (0.1 sec) - GPS is connected but does not yet have a fix. 89 | * Red 2 Slow Blinks (0.5 sec) - GPS has a 2D-only fix; position data is being read. 90 | * Red 1 Slow Blink (0.5s) - GPS has a 3D fix; position data is being read. 91 | * Green Blink (0.25s) - Data point written to disk (and flushed, if flush not disabled). 92 | 93 | Waveshare 2.13-inch e-Ink Display Hat B 94 | +++++++++++++++++++++++++++++++++++++++ 95 | 96 | This 128x32 monochrome OLED display has a fixed pinout, and plugs directly in to the Pi's 40-pin GPIO header. **You must enable SPI via ``raspi-config`` before it will work.** The display is extremely sluggish, and takes approximately 15 seconds to redraw the image. It does not support partial re-draw, though some of their other models do. 97 | 98 | This display has a driver built-in to pizero-gpslog. To use the display, set ``DISPLAY_CLASS`` to ``pizero_gpslog.displays.epd2in13bc:EPD2in13bc``. 99 | 100 | Displays can be tested with some sample data using the ``pizero-gpslog-screentest`` entrypoint. 101 | 102 | Adafruit 4567 2.23" Monochrome OLED Bonnet 103 | ++++++++++++++++++++++++++++++++++++++++++ 104 | 105 | This display uses I2C and connects to the Pi's 40-pin GPIO header. **You must enable I2C via ``raspi-config`` before it will work.** The display refreshes quite quickly (up to 30Hz) but draws considerably more power than the e-Ink displays. 106 | 107 | This display driver requires the installation of the `adafruit-circuitpython-ssd1305 `_ Python package. 108 | 109 | This display has a driver built-in to pizero-gpslog. To use the display, set ``DISPLAY_CLASS`` to ``pizero_gpslog.displays.adafruit4567:Adafruit4567``. 110 | 111 | Displays can be tested with some sample data using the ``pizero-gpslog-screentest`` entrypoint. 112 | 113 | Your Own Display 114 | ++++++++++++++++ 115 | 116 | pizero-gpslog can support "any" display that's capable of rendering text. To implement a display driver class, create a subclass of ``pizero_gpslog.displays.base.BaseDisplay`` and implement at least the required methods and properties, as well as whatever internals your display needs. See the ``Adafruit4567`` class as an example. While it is strongly encouraged for you to contribute any display drivers back to the project via pull requests, the import system used can import any class from any importable module. 117 | 118 | Displays can be tested with some sample data using the ``pizero-gpslog-screentest`` entrypoint. 119 | 120 | Extra Data Providers 121 | -------------------- 122 | 123 | It's possible to have a dict of arbitrary data from a "data provider" - a class to read any arbitrary sensor - included in each GPS location line in the output file. Extra Data Providers must be classes which are subclasses of ``pizero_gpslog.extradata.base.BaseExtraDataProvider``, implement all of its methods, and set ``self._data`` to a dict. the dict should have two keys: ``message``, a string message suitable for a line on a display (e.g. 20 characters or less), and ``data``, an arbitrary JSON-encodeable dict. 124 | 125 | Data providers are enabled by setting the ``EXTRA_DATA_CLASS`` environment variable to the module name and class name in colon-separated format. 126 | 127 | Two data providers are included: 128 | 129 | * Dummy ExtraData can be generated by running with ``EXTRA_DATA_CLASS=pizero_gpslog.extradata.dummy:DummyData`` 130 | * GQ Electronics GMC-series geiger counter sensors can be enabled by running with ``EXTRA_DATA_CLASS=pizero_gpslog.extradata.gq_gmc500plus:GqGMC500plus``. This currently requires using my fork, i.e. ``pip install git+https://gitlab.com/jantman/gmc.git@jantman-fixes-config`` 131 | 132 | Configuration 133 | ------------- 134 | 135 | pizero-gpslog's entire configuration is provided via environment variables. There are NO command-line switches. By default, these are set in ``/etc/systemd/system/pizero-gpslog.service`` by the ``pizero-gpslog-install`` installer script and need to be updated in that file. 136 | 137 | * ``LOG_LEVEL`` - Defaults to "WARNING"; other accepted values are "INFO" and "DEBUG". All logging is to STDOUT. 138 | * ``LED_PIN_RED`` - Integer. Specifies the GPIO pin number used for the primary ("red") LED. Leave unset if running on non-RPi hardware (in which case LED state will be logged to STDOUT) or if using a display. Note the number used here is the Broadcom GPIO pin number, not the physical board pin number. 139 | * ``LED_PIN_GREEN`` - Integer. Specifies the GPIO pin number used for the secondary ("green") LED. Leave unset if running on non-RPi hardware (in which case LED state will be logged to STDOUT) or if using a display. Note the number used here is the Broadcom GPIO pin number, not the physical board pin number. 140 | * ``GPS_INTERVAL_SEC`` - Integer. Interval to poll gps at, and write gps position. Defaults to every 5 seconds. 141 | * ``FLUSH_FILE`` - String. If set to "false", do not explicitly flush output file after every write. 142 | * ``OUT_DIR`` - Directory to write log files under. If not set, will use current working directory (when running via systemd, as default, this will be the current directory that the installer was run in). 143 | * ``DISPLAY_CLASS`` - String. The colon-separated module path and class name of an importable class to drive a display. See details above on using displays. 144 | * ``DISPLAY_REFRESH_SEC`` - Integer. The ideal/target number of seconds between display refreshes. Note that how fast a display can actually refresh is hardware-specific, and how fast you *want* it to refresh is based on its power consumption and your battery life. The default value for this parameter is to refresh **as quickly as the display will allow!** If you use a fast display, you should set this to a sane integer. 145 | 146 | Running 147 | ------- 148 | 149 | Configure as described above. Plug the Pi into a power source. Everything else should be automatic after the installation described above. The ``pizero-gpslog`` systemd service should start automatically at boot. 150 | 151 | Log Files 152 | +++++++++ 153 | 154 | Log files will be written under the directory specified by the ``OUT_DIR`` environment variable, or the current working directory if that environment variable is not set. Log files will be written under that directory, named according to the time and date when the program started (``%Y-%m-%d_%H-%M-%S`` format). 155 | 156 | Each line of the output file is a single raw gpsd response to the ``?POLL`` command. While this program also decodes the responses, it doesn't make sense for us to invent our own data structure for something that already has one. Each line in the output file should be valid JSON matching the `gpsd JSON ?POLL response schema `_, deserialized and reserialized to ensure that it does not contain any linebreaks. 157 | 158 | Getting the Data 159 | ++++++++++++++++ 160 | 161 | At the moment, when I'm home from a hike and the Pi is powered down, I just pull the SD card and copy the data to my computer, then delete the data file(s) from the SD card and put it back. It would certainly be easy to automate this with a Pi Zero W or an Ethernet or WiFi connection, but it's not worth it for me for this project. If you're interested, I have some scripts and instructions that might help as part of my `pi2graphite `_ project. 162 | 163 | Using the Data 164 | -------------- 165 | 166 | The log files output by ``pizero-gpslog`` are in the `gpsd JSON ?POLL response format `_, one response per line (some responses may be empty). In order to make the output useful, this package also includes the ``pizero-gpslog-convert`` command line tool which can convert a specified JSON file to one of a variety of more-useful formats. While `gpsbabel `_ is the standard for GPS data format conversion, it doesn't support the gpsd POLL response format. This utility is provided as a means of converting to some common GPS data formats. If you need other formats, please convert to one of these and then to gpsbabel. 167 | 168 | * ``pizero-gpslog-convert YYYY-MM-DD_HH:MM:SS.json`` - convert ``YYYY-MM-DD_HH:MM:SS.json`` to GPX and write at ``YYYY-MM-DD_HH:MM:SS.gpx`` 169 | * ``pizero-gpslog-convert --stats YYYY-MM-DD_HH:MM:SS.json`` - same as above, but also print some stats to STDERR 170 | 171 | It's up to you how to use the data, but there are a number of handy online tools that work with GPX files, including: 172 | 173 | * `gpsvisualizer.com `_ that has multiple output formats including `elevation and speed profiles `_ (and other profiles including slope, climb rate, pace, etc.), plotting the track `on Google Maps `_ (including with colorization by speed, elevation, slope, climb rate, pace, etc.), converting `to Google Earth KML `_, etc. Plotting can also use sources other than Google Maps, such as OpenStreetMap, ThunderForest, OpenTopoMap, USGS, USFS, etc. (and there's some `explanation `_ about how this is done). 174 | * `utrack.crempa.net `_ Takes a GPX file and generates a HTML page "report" giving a map overlay (with optional elevation colorization) as well as elevation and speed profiles (against both time and distance), some statistics, a distance vs time profile, and the option to download that report as a PDF. 175 | * `sunearthtools.com `_ has a simple tool (admittedly with a poor UI) that plots GPX data on Google maps along with a separate speed and elevation profile (by distance). 176 | * `mygpsfiles `_ Is a web-based app with a native-looking tiled UI that can plot tracks on Google Maps (Satellite or Map + Topo) as well as displaying per-point statistics (distance, time, elevation, speed, pace) and a configurable profile of elevation, speed, distance, pace, etc. As far as I can tell, all units are metric. 177 | 178 | Testing 179 | ------- 180 | 181 | There currently aren't any code tests. But there are some scripts and tox-based helpers to aid with manual testing. 182 | 183 | * ``pizero_gpslog/tests/data/runfake.sh`` - Runs `gpsfake `_ (provided by gpsd) with sample data. Takes optional arguments for ``--nofix`` (data with no GPS fix) or ``--stillfix`` (fix but not moving). 184 | * Running with ``DISPLAY_CLASS=pizero_gpslog.displays.dummy:DummyDisplay`` will output display lines to STDOUT. 185 | * Dummy ExtraData can be generated by running with ``EXTRA_DATA_CLASS=pizero_gpslog.extradata.dummy:DummyData``. 186 | 187 | Development 188 | ----------- 189 | 190 | Follow normal installation instructions, but instead of ``sudo pip3 install pizero-gpslog && sudo pizero-gpslog-install``, log in as ``pi``, and in ``/home/pi`` run ``git clone https://github.com/jantman/pizero-gpslog.git && cd pizero_gpslog/ && sudo python3 setup.py develop && sudo pizero-gpslog-install``. 191 | 192 | Release Process 193 | --------------- 194 | 195 | 1. Test changes locally, ensure they work as desired. 196 | 1. Ensure the version number has been incremented and there's an entry in ``CHANGES.rst`` 197 | 1. Merge PR to ``master`` branch. 198 | 1. Manually tag master with the new version number and create a GitHub Release for it. 199 | 1. The above will trigger TravisCI to build and push to PyPI. 200 | 201 | Acknowledgements 202 | ---------------- 203 | 204 | First, many thanks to the developers of gpsd, who have put forth the massive effort to make a script like this relatively trivial. 205 | 206 | Second, thanks to `Martijn Braam `_, developer of the MIT-licensed `gpsd-py3 `_ package. A modified version of that package makes up the ``gpsd.py`` module. 207 | --------------------------------------------------------------------------------