pat/internal/gpsd/gpsd.go
Martin Hebnes Pedersen f0df19a6af go fmt ./...
2023-10-21 13:10:19 +02:00

237 lines
4.9 KiB
Go

package gpsd
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
)
type NMEAMode int
const (
ModeUnknown NMEAMode = iota
ModeNoFix
Mode2D
Mode3D
)
var ErrUnsupportedProtocolVersion = errors.New("unsupported protocol version")
// Positioner implementations provide geographic positioning data.
//
// This is particularly useful for testing if an object returned by Next can be used to determine the device position.
type Positioner interface {
Position() Position
HasFix() bool
}
// Position holds geographic positioning data.
type Position struct {
Lat, Lon float64 // Latitude/longitude in degrees. +/- signifies north/south.
Alt float64 // Altitude in meters.
Track float64 // Course over ground, degrees from true north.
Speed float64 // Speed over ground, meters per second.
Time time.Time // Time as reported by the device.
}
// Conn represents a socket connection to an GPSd daemon.
type Conn struct {
Version Version
mu sync.Mutex
tcpConn net.Conn
rd *bufio.Reader
watchEnabled bool
closed bool
}
// Dial establishes a socket connection to the GPSd daemon.
func Dial(addr string) (*Conn, error) {
tcpConn, err := net.DialTimeout("tcp", addr, 30*time.Second)
if err != nil {
return nil, err
}
c := &Conn{
tcpConn: tcpConn,
rd: bufio.NewReader(tcpConn),
}
err = json.NewDecoder(c.rd).Decode(&c.Version)
if err != nil || c.Version.Release == "" {
tcpConn.Close()
return nil, errors.New("unexpected server response")
}
if c.Version.ProtoMajor < 3 {
tcpConn.Close()
return nil, ErrUnsupportedProtocolVersion
}
return c, nil
}
// Watch enables or disables the watcher mode.
//
// In watcher mode, GPS reports are dumped as TPV and SKY objects. These objects are available through the Next method.
func (c *Conn) Watch(enable bool) bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return false
}
if enable == c.watchEnabled {
return enable
}
c.tcpConn.SetDeadline(time.Now().Add(30 * time.Second))
defer c.tcpConn.SetDeadline(time.Time{})
param, _ := json.Marshal(
map[string]interface{}{
"class": "WATCH",
"enable": enable,
"json": true,
})
c.send("?WATCH=%s", param)
for {
obj, err := c.next()
if err != nil {
return false
}
if watch, ok := obj.(watch); ok {
c.watchEnabled = watch.Enable
break
}
}
return c.watchEnabled
}
// Close closes the GPSd daemon connection.
func (c *Conn) Close() error {
c.Watch(false)
c.closed = true
return c.tcpConn.Close()
}
// Next returns the next object sent from the daemon, or an error.
//
// The empty interface returned can be any of the following types:
// - Sky: A Sky object reports a sky view of the GPS satellite positions.
// - TPV: A TPV object is a time-position-velocity report.
func (c *Conn) Next() (interface{}, error) {
c.mu.Lock()
defer c.mu.Unlock()
for {
obj, err := c.next()
if err != nil {
return nil, err
}
switch obj.(type) {
case TPV, Sky:
return obj, nil
default:
// Ignore other objects for now.
}
}
}
func (c *Conn) next() (interface{}, error) {
line, err := c.rd.ReadBytes('\n')
if err != nil {
return nil, err
}
return parseJSONObject(line)
}
var (
ErrTimeout = errors.New("timeout")
ErrWatchModeEnabled = errors.New("operation not available while in watch mode")
)
// NextPos returns the next reported position.
func (c *Conn) NextPos() (Position, error) {
return c.NextPosTimeout(0)
}
// NextPosTimeout returns the next reported position, or an empty position on timeout.
func (c *Conn) NextPosTimeout(timeout time.Duration) (Position, error) {
var deadline time.Time
if timeout > 0 {
deadline = time.Now().Add(timeout)
c.tcpConn.SetDeadline(deadline)
defer c.tcpConn.SetDeadline(time.Time{})
}
for {
obj, err := c.Next()
var netErr net.Error
if ok := errors.As(err, &netErr); ok && netErr.Timeout() {
return Position{}, ErrTimeout
} else if err != nil {
return Position{}, err
}
if pos, ok := obj.(Positioner); ok && pos.HasFix() {
return pos.Position(), nil
}
if !deadline.IsZero() && time.Now().After(deadline) {
return Position{}, ErrTimeout
}
}
}
// Devices returns a list of all devices GPSd is aware of.
//
// ErrWatchModeEnabled will be returned if the connection is in watch mode.
// A nil-slice will be returned if the connection has been closed.
func (c *Conn) Devices() ([]Device, error) {
if c.closed {
return nil, nil
} else if c.watchEnabled {
return nil, ErrWatchModeEnabled
}
c.mu.Lock()
defer c.mu.Unlock()
c.send("?DEVICES;")
for {
obj, err := c.next()
if err != nil {
return nil, errUnexpected(err)
}
if devs, ok := obj.([]Device); ok {
return devs, nil
}
}
}
func (c *Conn) send(s string, params ...interface{}) error {
_, err := fmt.Fprintf(c.tcpConn, s, params...)
return errUnexpected(err)
}
func errUnexpected(err error) error {
if errors.Is(err, io.EOF) {
err = io.ErrUnexpectedEOF
}
return err
}