mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
45fad80c3e
Adafruit's Mini GPS PA1010D Module works with this device driver, but requires 0x10 as the address, rather than 0x42. This change allows the device to be initialised with whatever i2c address is needed, while maintaining backward compatibility. Adds new constants to allow easy configuration of both the ublox device and the PA1010D.
190 lines
4.8 KiB
Go
190 lines
4.8 KiB
Go
// Package gps provides a driver for GPS receivers over UART and I2C
|
|
package gps // import "tinygo.org/x/drivers/gps"
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"tinygo.org/x/drivers"
|
|
)
|
|
|
|
var (
|
|
errInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length")
|
|
errInvalidNMEAChecksum = errors.New("invalid NMEA sentence checksum")
|
|
errEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
|
|
errUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
|
|
errInvalidGGASentence = errors.New("invalid GGA NMEA sentence")
|
|
errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence")
|
|
errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence")
|
|
)
|
|
|
|
type GPSError struct {
|
|
Err error
|
|
Info string
|
|
Sentence string
|
|
}
|
|
|
|
func newGPSError(err error, sentence string, info string) GPSError {
|
|
return GPSError{
|
|
Info: info,
|
|
Err: err,
|
|
Sentence: sentence,
|
|
}
|
|
}
|
|
|
|
func (ge GPSError) Error() string {
|
|
return ge.Err.Error() + " " + ge.Info + " " + ge.Sentence
|
|
}
|
|
|
|
func (ge GPSError) Unwrap() error {
|
|
return ge.Err
|
|
}
|
|
|
|
const (
|
|
minimumNMEALength = 7
|
|
startingDelimiter = '$'
|
|
checksumDelimiter = '*'
|
|
)
|
|
|
|
// Device wraps a connection to a GPS device.
|
|
type Device struct {
|
|
buffer []byte
|
|
bufIdx int
|
|
sentence strings.Builder
|
|
uart drivers.UART
|
|
bus drivers.I2C
|
|
address uint16
|
|
}
|
|
|
|
// NewUART creates a new UART GPS connection. The UART must already be configured.
|
|
func NewUART(uart drivers.UART) Device {
|
|
return Device{
|
|
uart: uart,
|
|
buffer: make([]byte, bufferSize),
|
|
bufIdx: bufferSize,
|
|
sentence: strings.Builder{},
|
|
}
|
|
}
|
|
|
|
// NewI2C creates a new I2C GPS connection.
|
|
// Uses the default i2c address (0x42) for backward compatibility reasons.
|
|
func NewI2C(bus drivers.I2C) Device {
|
|
return NewI2CWithAddress(bus, I2C_ADDRESS)
|
|
}
|
|
|
|
// NewI2CWithAddress creates a new I2C GPS connection on the provided address
|
|
func NewI2CWithAddress(bus drivers.I2C, i2cAddress uint16) Device {
|
|
return Device{
|
|
bus: bus,
|
|
address: i2cAddress,
|
|
buffer: make([]byte, bufferSize),
|
|
bufIdx: bufferSize,
|
|
sentence: strings.Builder{},
|
|
}
|
|
}
|
|
|
|
// NextSentence returns the next valid NMEA sentence from the GPS device.
|
|
func (gps *Device) NextSentence() (sentence string, err error) {
|
|
sentence = gps.readNextSentence()
|
|
if err = validSentence(sentence); err != nil {
|
|
return "", err
|
|
}
|
|
return sentence, nil
|
|
}
|
|
|
|
// readNextSentence returns the next sentence from the GPS device.
|
|
func (gps *Device) readNextSentence() (sentence string) {
|
|
gps.sentence.Reset()
|
|
var b byte = ' '
|
|
|
|
for b != startingDelimiter {
|
|
b = gps.readNextByte()
|
|
}
|
|
|
|
for b != checksumDelimiter {
|
|
gps.sentence.WriteByte(b)
|
|
b = gps.readNextByte()
|
|
}
|
|
gps.sentence.WriteByte(b)
|
|
gps.sentence.WriteByte(gps.readNextByte())
|
|
gps.sentence.WriteByte(gps.readNextByte())
|
|
|
|
sentence = gps.sentence.String()
|
|
return sentence
|
|
}
|
|
|
|
func (gps *Device) readNextByte() (b byte) {
|
|
gps.bufIdx += 1
|
|
if gps.bufIdx >= bufferSize {
|
|
gps.fillBuffer()
|
|
}
|
|
return gps.buffer[gps.bufIdx]
|
|
}
|
|
|
|
func (gps *Device) fillBuffer() {
|
|
if gps.uart != nil {
|
|
gps.uartFillBuffer()
|
|
} else {
|
|
gps.i2cFillBuffer()
|
|
}
|
|
}
|
|
|
|
func (gps *Device) uartFillBuffer() {
|
|
for gps.uart.Buffered() < bufferSize {
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
gps.uart.Read(gps.buffer[0:bufferSize])
|
|
gps.bufIdx = 0
|
|
}
|
|
|
|
func (gps *Device) i2cFillBuffer() {
|
|
for gps.available() < bufferSize {
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
gps.bus.Tx(gps.address, []byte{DATA_STREAM_REG}, gps.buffer[0:bufferSize])
|
|
gps.bufIdx = 0
|
|
}
|
|
|
|
// Available returns how many bytes of GPS data are currently available.
|
|
func (gps *Device) available() (available int) {
|
|
var lengthBytes [2]byte
|
|
gps.bus.Tx(gps.address, []byte{BYTES_AVAIL_REG}, lengthBytes[0:2])
|
|
available = int(lengthBytes[0])*256 + int(lengthBytes[1])
|
|
return available
|
|
}
|
|
|
|
// WriteBytes sends data/commands to the GPS device
|
|
func (gps *Device) WriteBytes(bytes []byte) {
|
|
if gps.uart != nil {
|
|
gps.uart.Write(bytes)
|
|
} else {
|
|
gps.bus.Tx(gps.address, []byte{}, bytes)
|
|
}
|
|
}
|
|
|
|
// validSentence checks if a sentence has been received uncorrupted
|
|
// For example, a valid NMEA sentence such as this:
|
|
// $GPGLL,3751.65,S,14507.36,E*77
|
|
// It has to start with a '$' character.
|
|
// It has to have a 5 character long sentence identifier.
|
|
// It has to end with a '*' character following by a checksum.
|
|
func validSentence(sentence string) error {
|
|
if len(sentence) < minimumNMEALength || sentence[0] != startingDelimiter || sentence[len(sentence)-3] != checksumDelimiter {
|
|
return errInvalidNMEASentenceLength
|
|
}
|
|
var cs byte = 0
|
|
for i := 1; i < len(sentence)-3; i++ {
|
|
cs ^= sentence[i]
|
|
}
|
|
checksum := strings.ToUpper(hex.EncodeToString([]byte{cs}))
|
|
if checksum != sentence[len(sentence)-2:len(sentence)] {
|
|
return newGPSError(errInvalidNMEAChecksum, sentence,
|
|
"expected "+sentence[len(sentence)-2:len(sentence)]+
|
|
" got "+checksum)
|
|
}
|
|
|
|
return nil
|
|
}
|