Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated grid square calculations with GPSd #435

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,25 @@ require (
github.com/pd0mz/go-maidenhead v1.0.0
github.com/peterh/liner v1.2.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.1
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/albenik/go-serial/v2 v2.6.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/la5nta/wl2k-go v0.7.3/go.mod h1:rTQaxPiAFD3pWGWN8Lh+BskN3Fpii84GoVwpTHNiCjE=
github.com/la5nta/wl2k-go v0.11.5/go.mod h1:0c+/9KyDj7Ra7C/O4rVUYx1CzvdtS65di/93wlI22fo=
Expand Down Expand Up @@ -70,6 +72,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tarm/goserial v0.0.0-20151007205400-b3440c3c6355/go.mod h1:jcMo2Odv5FpDA6rp8bnczbUolcICW6t54K3s9gOlgII=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
Expand All @@ -95,6 +98,7 @@ golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
39 changes: 34 additions & 5 deletions internal/gpsd/gpsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/pd0mz/go-maidenhead"
"io"
"net"
"sync"
Expand Down Expand Up @@ -32,11 +33,12 @@ type Positioner interface {

// 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.
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.
GridSquare string // GridSquare calculated using Maidenhead Locator System
}

// Conn represents a socket connection to an GPSd daemon.
Expand All @@ -50,6 +52,12 @@ type Conn struct {
closed bool
}

type GPSdClient interface {
Dial(addr string) (GPSdClient, error)
Watch(enable bool)
Next() (*TPV, error)
}

Comment on lines +55 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused? If so, please omit it 😊

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am looking at storing the Locator in the user.json for the multi-user reloader. Like KM4ACK's user swap by config swap
in the PAT Menu 12. GPSd is great and I have one pi feeding 4 pi's locator. only have a 20 minute gap every other month.
in mailbox/CallSign/user.json is (CALLSIGN, Name, TAC), Locator (EL29ER27), WinLink/user (Password), Roll (Owner, Radio Op, Non-Ham), On Duty(00-23), NTS (routing itu-r2, NOAM, USA, TX, STX,) HW ACCESS (Telnet, Wifi2, Wifi5, Ardop, ### Ax25, VaraHF, VaraFM, VaraSat)
Just have to get the Variables loaded at the right time for each Network http connection session or postoffice push/pull.
This lets windows and mac users to get multi-user.... Roll->Multi-user->PostOffice->NTS
Herbert KD5PQJ

// Dial establishes a socket connection to the GPSd daemon.
func Dial(addr string) (*Conn, error) {
tcpConn, err := net.DialTimeout("tcp", addr, 30*time.Second)
Expand Down Expand Up @@ -235,3 +243,24 @@ func errUnexpected(err error) error {
}
return err
}

// GetGridSquare provides function for getting updated position and calculating the grid square.
func (c *Conn) GetGridSquare() (string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe NextGridSquare() would be a better name for this, to better harmonize with Next() and NextPos() 🙂

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely!

for {
obj, err := c.NextPos()
if err != nil {
return "", err
}

var lat = obj.Lat
var lon = obj.Lon

point := maidenhead.NewPoint(lat, lon)
newGridSquare, err := point.GridSquare()
if err != nil {
return "", err
}

return newGridSquare, nil
}
}
27 changes: 25 additions & 2 deletions internal/gpsd/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gpsd
import (
"encoding/json"
"errors"
"github.com/pd0mz/go-maidenhead"
"time"
)

Expand All @@ -27,6 +28,23 @@ type TPV struct {
EPX, EPY, EPV json.Number // Lat, Lon, Alt error estimate in meters, 95% confidence. Present if mode is 2 or 3 and DOPs can be calculated from the satellite view.
Track, Speed, Climb json.Number
EPD, EPS, EPC json.Number
GridSquare string `json:"-"`
}

func (t *TPV) CalculateGridSquare() error {
lat, err := t.Lat.Float64()
if err != nil {
return err
}

lon, err := t.Lon.Float64()
if err != nil {
return err
}

point := maidenhead.NewPoint(lat, lon)
t.GridSquare, err = point.GridSquare()
return err
Comment on lines +31 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer needed I believe? conn.GetGridSquare() is not using the TPV's GridSquare, but calculates from LatLon on the fly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Martin!

You are correct...it is an artifact that I forgot to remove. Thanks!

}

func (t TPV) Position() Position {
Expand All @@ -35,8 +53,9 @@ func (t TPV) Position() Position {
alt, _ := t.Alt.Float64()
track, _ := t.Track.Float64()
speed, _ := t.Speed.Float64()
gridsquare := t.GridSquare
martinhpedersen marked this conversation as resolved.
Show resolved Hide resolved

return Position{Lat: lat, Lon: lon, Alt: alt, Track: track, Speed: speed, Time: t.Time}
return Position{Lat: lat, Lon: lon, Alt: alt, Track: track, Speed: speed, Time: t.Time, GridSquare: gridsquare}
}

func (t TPV) HasFix() bool { return t.Mode > ModeNoFix }
Expand Down Expand Up @@ -131,7 +150,11 @@ func parseJSONObject(raw []byte) (interface{}, error) {
case "TPV":
var tpv TPV
err = json.Unmarshal(raw, &tpv)
return tpv, err
err = tpv.CalculateGridSquare()
if err != nil {
return nil, err
}
return tpv, nil
martinhpedersen marked this conversation as resolved.
Show resolved Hide resolved
default:
var m map[string]interface{}
err = json.Unmarshal(raw, &m)
Expand Down
45 changes: 45 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ var fOptions struct {
IgnoreBusy bool // Move to connect?
SendOnly bool // Move to connect?
RadioOnly bool
UpdateGrid bool

Robust bool
MyCall string
Expand All @@ -214,6 +215,7 @@ func optionsSet() *pflag.FlagSet {
set.BoolVarP(&fOptions.SendOnly, "send-only", "s", false, "Download inbound messages later, send only.")
set.BoolVarP(&fOptions.RadioOnly, "radio-only", "", false, "Radio Only mode (Winlink Hybrid RMS only).")
set.BoolVar(&fOptions.IgnoreBusy, "ignore-busy", false, "Don't wait for clear channel before connecting to a node.")
set.BoolVar(&fOptions.UpdateGrid, "gridsquare-update", true, "Automatically update the maidenhead grid square from the gpsd daemon.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use the term locator in the config. Maybe not the best term, but I think we should use the same term both places sice the two are very much related.

--locator-from-gpsd maybe?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Martin!

Yes, I will change to align with the existing terminology. It makes the most sense to keep things consistent.


defaultMBox := filepath.Join(directories.DataDir(), "mailbox")
defaultFormsPath := filepath.Join(directories.DataDir(), "Standard_Forms")
Expand Down Expand Up @@ -264,6 +266,7 @@ func main() {
debug.Printf("Config file is\t'%s'", fOptions.ConfigPath)
debug.Printf("Log file is \t'%s'", fOptions.LogPath)
debug.Printf("Event log file is\t'%s'", fOptions.EventLogPath)
debug.Printf("Auto gridsquare update is\t'%s'", fOptions.UpdateGrid)
directories.MigrateLegacyDataDir()

// Graceful shutdown by cancelling background context on interrupt.
Expand Down Expand Up @@ -387,6 +390,48 @@ func main() {

// Start command execution
cmd.HandleFunc(ctx, args)

// Go routine for checking the GPS
go func() {
if fOptions.UpdateGrid {
for range time.Tick(time.Minute * 5) {
updateGridIfNeeded()
}
Comment on lines +397 to +399
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for polling once per 5 minute? Wouldn't it be even better to continuously keep the locator field up-to-date by subscribing (Watch(true)) and call Next() in an endless loop? 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was that while most people will be mostly static...i.e. once they get the first fix from the GPS, they won't need it anymore. However, for true mobile operation, it might be required to get a new grid square at a regular interval.

I picked 5 minutes at random for testing, but I think long term, this might be something we allow the user to enable/disable and set the interval as needed. If you were in a moving car for example, 5 minutes might be a good interval, but on a sailboat, it might need be only once every few hours.

I'll disable the loop for now.

}
}()
}

// Update Grid if needed
func updateGridIfNeeded() {
// create connection to gpsd
conn, err := gpsd.Dial(config.GPSd.Addr)
if err != nil {
log.Printf("GPSd daemon: %s", err)
return
}
defer conn.Close()
conn.Watch(true)

// get next position from gpsd
pos, err := conn.NextPos()
martinhpedersen marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
log.Printf("GPSd: %s", err)
return
}

// get grid square from position
currGridSquare := pos.GridSquare

if currGridSquare != config.Locator {
// update config
config.Locator = currGridSquare
// write config
if err := WriteConfig(config, fOptions.ConfigPath); err != nil {
log.Printf("Unable to write config: %s", err)
}
Comment on lines +428 to +431
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think rewriting the actual config file is the best idea. It is not a lossless operation, as the user might have defined keys and values that are not part of the JSON spec (e.g. "comments"). Custom formatting will also be lost, and possibly also ordering of aliases etc. It also has the potential of corrupting the file if we for some reason the operation fails in the middle of writing (full disk, power loss++).

I guess it's useful to retain the "last known" position when restarting the app. Otherwise, the user would have to wait the 5 min interval before the locator field is updated again

As I see it, we have two options:

  1. Persist the "last known position" in a separate JSON file, and use that as a initial value (if present).
  2. Block on startup until we get a fresh position from GPSd.

I think the latter might be a better solution, as it guarantees running with --locator-from-gpsd uses a value from GPSd at all times.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Martin...I agree the latter would be the better option.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some refreshing on this...won't the gaps.Dial() and conn.NextPos() already blocking?

// log
log.Printf("Grid square updated to %s", currGridSquare)
}
}

func configureHandle(ctx context.Context, args []string) {
Expand Down