Initial commit.

This commit is contained in:
Tony DiCola 2017-11-07 15:32:14 -08:00
commit 885359f367
10 changed files with 653 additions and 0 deletions

53
.travis.yml Normal file
View file

@ -0,0 +1,53 @@
# Travis CI configuration for automated .mpy file generation.
# Author: Tony DiCola
# License: Public Domain
# This configuration will work with Travis CI (travis-ci.org) to automacially
# build .mpy files for CircuitPython when a new tagged release is created. This
# file is relatively generic and can be shared across multiple repositories by
# following these steps:
# 1. Copy this file into a .travis.yml file in the root of the repository.
# 2. Change the deploy > file section below to list each of the .mpy files
# that should be generated. The config will automatically look for
# .py files with the same name as the source for generating the .mpy files.
# Note that the .mpy extension should be lower case!
# 3. Commit the .travis.yml file and push it to GitHub.
# 4. Go to travis-ci.org and find the repository (it needs to be setup to access
# your github account, and your github account needs access to write to the
# repo). Flip the 'ON' switch on for Travis and the repo, see the Travis
# docs for more details: https://docs.travis-ci.com/user/getting-started/
# 5. Get a GitHub 'personal access token' which has at least 'public_repo' or
# 'repo' scope: https://help.github.com/articles/creating-an-access-token-for-command-line-use/
# Keep this token safe and secure! Anyone with the token will be able to
# access and write to your GitHub repositories. Travis will use the token
# to attach the .mpy files to the release.
# 6. In the Travis CI settings for the repository that was enabled find the
# environment variable editing page: https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings
# Add an environment variable named GITHUB_TOKEN and set it to the value
# of the GitHub personal access token above. Keep 'Display value in build
# log' flipped off.
# 7. That's it! Tag a release and Travis should go to work to add .mpy files
# to the release. It takes about a 2-3 minutes for a worker to spin up,
# build mpy-cross, and add the binaries to the release.
language: generic
sudo: true
deploy:
provider: releases
api_key: $GITHUB_TOKEN
file:
- "adafruit_gps.mpy"
skip_cleanup: true
on:
tags: true
before_install:
- sudo apt-get -yqq update
- sudo apt-get install -y build-essential git python python-pip
- git clone https://github.com/adafruit/circuitpython.git -b 2.x
- make -C circuitpython/mpy-cross
- export PATH=$PATH:$PWD/circuitpython/mpy-cross/
- sudo pip install shyaml
before_deploy:
- shyaml get-values deploy.file < .travis.yml | sed 's/.mpy/.py/' | xargs -L1 mpy-cross

74
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at support@adafruit.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Tony DiCola for Adafruit Industries
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

45
README.rst Normal file
View file

@ -0,0 +1,45 @@
Introduction
============
.. image:: https://readthedocs.org/projects/adafruit-circuitpython-gps/badge/?version=latest
:target: https://circuitpython.readthedocs.io/projects/gps/en/latest/
:alt: Documentation Status
.. image :: https://img.shields.io/discord/327254708534116352.svg
:target: https://discord.gg/nBQh6qu
:alt: Discord
GPS parsing module. Can parse simple NMEA data sentences from serial GPS
modules to read latitude, longitude, and more.
Dependencies
=============
This driver depends on:
* `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
Please ensure all dependencies are available on the CircuitPython filesystem.
This is easily achieved by downloading
`the Adafruit library and driver bundle <https://github.com/adafruit/Adafruit_CircuitPython_Bundle>`_.
Usage Example
=============
See examples/simple.py for a demonstration of parsing and printing GPS location.
Contributing
============
Contributions are welcome! Please read our `Code of Conduct
<https://github.com/adafruit/Adafruit_CircuitPython_gps/blob/master/CODE_OF_CONDUCT.md>`_
before contributing to help this project stay welcoming.
API Reference
=============
.. toctree::
:maxdepth: 2
api

220
adafruit_gps.py Normal file
View file

@ -0,0 +1,220 @@
# The MIT License (MIT)
#
# Copyright (c) 2017 Tony DiCola for Adafruit Industries
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`adafruit_gps`
====================================================
GPS parsing module. Can parse simple NMEA data sentences from serial GPS
modules to read latitude, longitude, and more.
* Author(s): Tony DiCola
"""
import time
# Internal helper parsing functions.
# These handle input that might be none or null and return none instead of
# throwing errors.
def _parse_degrees(nmea_data):
# Parse a NMEA lat/long data pair 'dddmm.mmmm' into a pure degrees value.
# Where ddd is the degrees, mm.mmmm is the minutes.
if nmea_data is None or len(nmea_data) < 3:
return None
raw = float(nmea_data)
deg = raw // 100
minutes = raw % 100
return deg + minutes/60
def _parse_int(nmea_data):
if nmea_data is None or nmea_data == '':
return None
return int(nmea_data)
def _parse_float(nmea_data):
if nmea_data is None or nmea_data == '':
return None
return float(nmea_data)
class GPS:
def __init__(self, uart):
self._uart = uart
# Initialize null starting values for GPS attributes.
self.timestamp_utc = None
self.latitude = None
self.longitude = None
self.fix_quality = None
self.satellites = None
self.horizontal_dilution = None
self.altitude_m = None
self.height_geoid = None
self.velocity_knots = None
self.track_angle_deg = None
def update(self):
"""Check for updated data from the GPS module and process it
accordingly. Returns True if new data was processed, and False if
nothing new was received.
"""
# Grab a sentence and check its data type to call the appropriate
# parsing function.
sentence = self._parse_sentence()
if sentence is None:
return False
data_type, args = sentence
data_type = data_type.upper()
if data_type == 'GPGGA': # GGA, 3d location fix
self._parse_GPGGA(args)
elif data_type == 'GPRMC': # RMC, minimum location info
self._parse_GPRMC(args)
return True
def send_command(self, command, add_checksum=True):
"""Send a command string to the GPS. If add_checksum is True (the
default) a NMEA checksum will automatically be computed and added.
Note you should NOT add the leading $ and trailing * to the command
as they will automatically be added!
"""
self._uart.write('$')
self._uart.write(command)
if add_checksum:
checksum = 0
for i in range(len(command)):
checksum ^= ord(command[i])
self._uart.write('*')
self._uart.write('{:02x}'.format(checksum).upper())
self._uart.write('\r\n')
@property
def has_fix(self):
"""True if a current fix for location information is available."""
return self.fix_quality is not None and self.fix_quality >= 1
def _parse_sentence(self):
# Parse any NMEA sentence that is available.
sentence = self._uart.readline()
if sentence is None or sentence == b'' or len(sentence) < 1:
return None
sentence = str(sentence, 'ascii').strip()
# Look for a checksum and validate it if present.
if len(sentence) > 7 and sentence[-3] == '*':
# Get included checksum, then calculate it and compare.
expected = int(sentence[-2:], 16)
actual = 0
for i in range(1, len(sentence)-3):
actual ^= ord(sentence[i])
if actual != expected:
return None # Failed to validate checksum.
# Remove checksum once validated.
sentence = sentence[:-3]
# Parse out the type of sentence (first string after $ up to comma)
# and then grab the rest as data within the sentence.
delineator = sentence.find(',')
if delineator == -1:
return None # Invalid sentence, no comma after data type.
data_type = sentence[1:delineator]
return (data_type, sentence[delineator+1:])
def _parse_GPGGA(self, args):
# Parse the arguments (everything after data type) for NMEA GPGGA
# 3D location fix sentence.
data = args.split(',')
if data is None or len(data) != 14:
return # Unexpected number of params.
# Parse fix time.
time_utc = _parse_float(data[0])
if time_utc is not None:
hours = time_utc // 10000
mins = int((time_utc // 100) % 100)
secs = time_utc % 100
# Set or update time to a friendly python time struct.
if self.timestamp_utc is not None:
self.timestamp_utc = time.struct_time((
self.timestamp_utc.tm_year, self.timestamp_utc.tm_mon,
self.timestamp_utc.tm_mday, hours, mins, secs, 0, 0, -1))
else:
self.timestamp_utc = time.struct_time((0, 0, 0, hours, mins,
secs, 0, 0, -1))
# Parse latitude and longitude.
self.latitude = _parse_degrees(data[1])
if self.latitude is not None and \
data[2] is not None and data[2].lower() == 's':
self.latitude *= -1.0
self.longitude = _parse_degrees(data[3])
if self.longitude is not None and \
data[4] is not None and data[4].lower() == 'w':
self.longitude *= -1.0
# Parse out fix quality and other simple numeric values.
self.fix_quality = _parse_int(data[5])
self.satellites = _parse_int(data[6])
self.horizontal_dilution = _parse_float(data[7])
self.altitude_m = _parse_float(data[8])
self.height_geoid = _parse_float(data[10])
def _parse_GPRMC(self, args):
# Parse the arguments (everything after data type) for NMEA GPRMC
# minimum location fix sentence.
data = args.split(',')
if data is None or len(data) < 11:
return # Unexpected number of params.
# Parse fix time.
self.fix_time_utc = _parse_float(data[0])
if self.fix_time_utc is not None:
# Set time to a friendly python time struct if available.
hours = self.fix_time_utc // 10000
mins = int((self.fix_time_utc // 100) % 100)
secs = self.fix_time_utc % 100
self.fix_time_utc = time.struct_time((0, 0, 0, hours, mins, secs, 0, 0, -1))
# Parse status (active/fixed or void).
status = data[1]
self.fix_quality = 0
if status is not None and status.lower() == 'a':
self.fix_quality = 1
# Parse latitude and longitude.
self.latitude = _parse_degrees(data[2])
if self.latitude is not None and \
data[3] is not None and data[3].lower() == 's':
self.latitude *= -1.0
self.longitude = _parse_degrees(data[4])
if self.longitude is not None and \
data[5] is not None and data[5].lower() == 'w':
self.longitude *= -1.0
# Parse out speed and other simple numeric values.
self.speed_knots = _parse_float(data[6])
self.track_angle_deg = _parse_float(data[7])
# Parse date.
if data[8] is not None and len(data[8]) == 6:
day = int(data[8][0:2])
month = int(data[8][2:4])
year = 2000 + int(data[8][4:6]) # Y2k bug, 2 digit date assumption.
# This is a problem with the NMEA
# spec and not this code.
if self.timestamp_utc is not None:
# Replace the timestamp with an updated one.
# (struct_time is immutable and can't be changed in place)
self.timestamp_utc = time.struct_time((year, month, day,
self.timestamp_utc.tm_hour, self.timestamp_utc.tm_min,
self.timestamp_utc.tm_sec, 0, 0, -1))
else:
# Time hasn't been set so create it.
self.timestamp_utc = time.struct_time((year, month, day, 0, 0,
0, 0, 0, -1))

5
api.rst Normal file
View file

@ -0,0 +1,5 @@
.. If you created a package, create one automodule per module in the package.
.. automodule:: adafruit_gps
:members:

142
conf.py Normal file
View file

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
]
intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None),'CircuitPython': ('https://circuitpython.readthedocs.io/en/latest/', None)}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'README'
# General information about the project.
project = u'Adafruit GPS Library'
copyright = u'2017 Tony DiCola'
author = u'Tony DiCola'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'1.0'
# The full version, including alpha/beta/rc tags.
release = u'1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
default_role = "any"
# If true, '()' will be appended to :func: etc. cross-reference text.
#
add_function_parentheses = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally
try:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), '.']
except:
html_theme = 'default'
html_theme_path = ['.']
else:
html_theme_path = ['.']
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Output file base name for HTML help builder.
htmlhelp_basename = 'AdafruitGPSLibrarydoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'AdafruitGPSLibrary.tex', u'Adafruit GPS Library Documentation',
author, 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'adafruitGPSlibrary', u'Adafruit GPS Library Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'AdafruitGPSLibrary', u'Adafruit GPS Library Documentation',
author, 'AdafruitGPSLibrary', 'One line description of project.',
'Miscellaneous'),
]

90
examples/simple.py Normal file
View file

@ -0,0 +1,90 @@
# Simple GPS module demonstration.
# Will wait for a fix and print a message every second with the current location
# and other details.
import board
import busio
import time
import adafruit_gps
# Define RX and TX pins for the board's serial port connected to the GPS.
# These are the defaults you should use for the GPS FeatherWing.
# For other boards set RX = GPS module TX, and TX = GPS module RX pins.
RX = board.RX
TX = board.TX
# Create a serial connection for the GPS connection using default speed and
# a slightly higher timeout (GPS modules typically update once a second).
uart = busio.UART(TX, RX, baudrate=9600, timeout=3000)
# Create a GPS module instance.
gps = adafruit_gps.GPS(uart)
# Initialize the GPS module by changing what data it sends and at what rate.
# These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and
# PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust
# the GPS module behavior:
# https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf
# Turn on the basic GGA and RMC info (what you typically want)
gps.send_command('PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0')
# Turn on just minimum info (RMC only, location):
#gps.send_command('PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0')
# Turn off everything:
#gps.send_command('PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0')
# Tuen on everything (not all of it is parsed!)
#gps.send_command('PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0')
# Set update rate to once a second (1hz) which is what you typically want.
gps.send_command('PMTK220,1000')
# Or decrease to once every two seconds by doubling the millisecond value.
# Be sure to also increase your UART timeout above!
#gps.send_command('PMTK220,2000')
# You can also speed up the rate, but don't go too fast or else you can lose
# data during parsing. This would be twice a second (2hz, 500ms delay):
#gps.send_command('PMTK220,500')
# Main loop runs forever printing the location, etc. every second.
last_print = time.monotonic()
while True:
# Make sure to call gps.update() every loop iteration and at least twice
# as fast as data comes from the GPS unit (usually every second).
# This returns a bool that's true if it parsed new data (you can ignore it
# though if you don't care and instead look at the has_fix property).
gps.update()
# Every second print out current location details if there's a fix.
current = time.monotonic()
if current - last_print >= 1.0:
last_print = current
if not gps.has_fix:
# Try again if we don't have a fix yet.
print('Waiting for fix...')
continue
# We have a fix! (gps.has_fix is true)
# Print out details about the fix like location, date, etc.
print('=' * 40) # Print a separator line.
print('Fix timestamp: {}/{}/{} {:02}:{:02}:{:02}'.format(
gps.timestamp_utc.tm_mon, # Grab parts of the time from the
gps.timestamp_utc.tm_mday, # struct_time object that holds
gps.timestamp_utc.tm_year, # the fix time. Note you might
gps.timestamp_utc.tm_hour, # not get all data like year, day,
gps.timestamp_utc.tm_min, # month!
gps.timestamp_utc.tm_sec))
print('Latitude: {} degrees'.format(gps.latitude))
print('Longitude: {} degrees'.format(gps.longitude))
print('Fix quality: {}'.format(gps.fix_quality))
# Some attributes beyond latitude, longitude and timestamp are optional
# and might not be present. Check if they're None before trying to use!
if gps.satellites is not None:
print('# satellites: {}'.format(gps.satellites))
if gps.altitude_m is not None:
print('Altitude: {} meters'.format(gps.altitude_m))
if gps.track_angle_deg is not None:
print('Speed: {} knots'.format(gps.speed_knots))
if gps.track_angle_deg is not None:
print('Track angle: {} degrees'.format(gps.track_angle_deg))
if gps.horizontal_dilution is not None:
print('Horizontal dilution: {}'.format(gps.horizontal_dilution))
if gps.height_geoid is not None:
print('Height geo ID: {} meters'.format(gps.height_geoid))

2
readthedocs.yml Normal file
View file

@ -0,0 +1,2 @@
requirements_file: requirements.txt

1
requirements.txt Normal file
View file

@ -0,0 +1 @@