mirror of
https://github.com/la5nta/pat
synced 2026-08-13 17:46:54 -04:00
237 lines
4.9 KiB
Go
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
|
|
}
|