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

Implement missing form template GPS-related insertion tags #330

Merged
merged 1 commit into from
Apr 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Copyright (c) 2020 Martin Hebnes Pedersen LA5NTA
* LA3QMA - Kai Günter Brandt
* LA4TTA - Erlend Grimseid
* LA5NTA - Martin Hebnes Pedersen
* N2YGK - Alan Crosswell
* VE7GNU - Doug Collinge
* W6IPA - JC Martin
* WY2K - Benjamin Seidenberg
Expand Down
11 changes: 8 additions & 3 deletions cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,15 @@ type GPSdConfig struct {
// Caution: Your GPS position will be accessible to any network device able to access Pat's HTTP interface.
EnableHTTP bool `json:"enable_http"`

// Use server time instead of timestamp provided by GPSd (e.g for older GPS
// device with week roll-over issue)
// Allow Winlink forms to use GPSd for aquiring your position.
//
// Caution: Your current GPS position will be automatically injected, without your explicit consent, into forms requesting such information.
AllowForms bool `json:"allow_forms"`

// Use server time instead of timestamp provided by GPSd (e.g for older GPS device with week roll-over issue).
UseServerTime bool `json:"use_server_time"`

// Address and port of GPSd server (e.g. localhost:2947)
// Address and port of GPSd server (e.g. localhost:2947).
Addr string `json:"addr"`
}

Expand Down Expand Up @@ -272,6 +276,7 @@ var DefaultConfig = Config{
},
GPSd: GPSdConfig{
EnableHTTP: false, // Default to false to help protect privacy of unknowing users (see github.com//issues/146)
AllowForms: false, // Default to false to help protect location privacy of unknowing users
UseServerTime: false,
Addr: "localhost:2947", // Default listen address for GPSd
},
Expand Down
119 changes: 119 additions & 0 deletions internal/forms/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"io"
"io/ioutil"
"log"
"math"
"net/http"
"os"
"path"
Expand All @@ -31,6 +32,10 @@ import (
"unicode/utf8"

"github.com/dimchansky/utfbom"
"github.com/la5nta/pat/cfg"
"github.com/la5nta/pat/internal/debug"
"github.com/la5nta/pat/internal/gpsd"
"github.com/pd0mz/go-maidenhead"
)

const (
Expand Down Expand Up @@ -60,6 +65,7 @@ type Config struct {
AppVersion string
LineReader func() string
UserAgent string
GPSd cfg.GPSdConfig
}

// Form holds information about a Winlink form template
Expand Down Expand Up @@ -705,6 +711,99 @@ func (m *Manager) findAbsPathForTemplatePath(tmplPath string) (string, error) {
return retVal, nil
}

// gpsPos returns the current GPS Position
func (m *Manager) gpsPos() (gpsd.Position, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Since the error returned from this function is never used for anything but to check for success, I think we should replace this with a "ok" boolean. Then you could omit the check for "noPos" as well 🙂

All these error messages could be useful for debugging, so I suggest logging them instead using our debug logger debug.Printf.

func (m *Manager) gpsPos() (pos gpsd.Position, ok bool) {

}

I'd be happy to do these last minute tweaks after merging if you prefer. Just say the word 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code currently propagates the return from NextPosTimeout() so this will become quite a bit uglier as would the NoPos checking since gpsFmt is called with a Position.

I've added a couple of debug Printf of the gpsPos error.

Feel free to make the tweaks you suggest if you disagree.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, I see what you mean 🙂

As a general rule of thumb I think it's better to either log or return an error, not both. It helps with readability and makes the responsibilities clearer IMO.

I think we should merge this now, and I'll see what can be done with regards to this afterwards.

Thank you 😄

addr := m.config.GPSd.Addr
if addr == "" {
return gpsd.Position{}, errors.New("GPSd: not configured.")
n2ygk marked this conversation as resolved.
Show resolved Hide resolved
}
if !m.config.GPSd.AllowForms {
return gpsd.Position{}, errors.New("GPSd: allow_forms is disabled. GPS position will not be available in form templates.")
}

conn, err := gpsd.Dial(addr)
if err != nil {
log.Printf("GPSd daemon: %s", err)
return gpsd.Position{}, err
}

defer conn.Close()

conn.Watch(true)

log.Println("Waiting for position from GPSd...")
// TODO: make the GPSd timeout configurable
return conn.NextPosTimeout(3 * time.Second)
}

type gpsStyle int

const (
// documentation: https://www.winlink.org/sites/default/files/RMSE_FORMS/insertion_tags.zip
signedDecimal gpsStyle = iota // 41.1234 -73.4567
decimal // 46.3795N 121.5835W
degreeMinute // 46-22.77N 121-35.01W
)

func gpsFmt(style gpsStyle, pos gpsd.Position) string {
n2ygk marked this conversation as resolved.
Show resolved Hide resolved
var (
northing string
easting string
latDegrees int
latMinutes float64
lonDegrees int
lonMinutes float64
)

noPos := gpsd.Position{}
if pos == noPos {
return "(Not available)"
}
switch style {
case degreeMinute:
{
latDegrees = int(math.Trunc(math.Abs(pos.Lat)))
latMinutes = (math.Abs(pos.Lat) - float64(latDegrees)) * 60
lonDegrees = int(math.Trunc(math.Abs(pos.Lon)))
lonMinutes = (math.Abs(pos.Lon) - float64(lonDegrees)) * 60
}
fallthrough
case decimal:
{
if pos.Lat >= 0 {
northing = "N"
} else {
northing = "S"
}
if pos.Lon >= 0 {
easting = "E"
} else {
easting = "W"
}
}
}

switch style {
case signedDecimal:
return fmt.Sprintf("%.4f %.4f", pos.Lat, pos.Lon)
case decimal:
return fmt.Sprintf("%.4f%s %.4f%s", math.Abs(pos.Lat), northing, math.Abs(pos.Lon), easting)
case degreeMinute:
return fmt.Sprintf("%02d-%05.2f%s %03d-%05.2f%s", latDegrees, latMinutes, northing, lonDegrees, lonMinutes, easting)
default:
return "(Not available)"
}
}

func posToGridSquare(pos gpsd.Position) string {
n2ygk marked this conversation as resolved.
Show resolved Hide resolved
point := maidenhead.NewPoint(pos.Lat, pos.Lon)
n2ygk marked this conversation as resolved.
Show resolved Hide resolved
gridsquare, err := point.GridSquare()
if err != nil {
return ""
}
return gridsquare
}

func (m *Manager) fillFormTemplate(absPathTemplate string, formDestURL string, placeholderRegEx *regexp.Regexp, formVars map[string]string) (string, error) {
fUnsanitized, err := os.Open(absPathTemplate)
if err != nil {
Expand Down Expand Up @@ -733,6 +832,15 @@ func (m *Manager) fillFormTemplate(absPathTemplate string, formDestURL string, p
nowDateUTC := now.UTC().Format("2006-01-02Z")
nowTimeUTC := now.UTC().Format("15:04:05Z")
udtg := strings.ToUpper(now.UTC().Format("021504Z Jan 2006"))
nowPos, err := m.gpsPos()
var validPos string
if err != nil {
validPos = "NO"
debug.Printf(fmt.Sprint(err))
} else {
validPos = "YES"
debug.Printf("GPSd position: %s", gpsFmt(signedDecimal, nowPos))
}

scanner := bufio.NewScanner(bytes.NewReader(sanitizedFileContent))
for scanner.Scan() {
Expand All @@ -750,6 +858,17 @@ func (m *Manager) fillFormTemplate(absPathTemplate string, formDestURL string, p
l = strings.ReplaceAll(l, "{UDTG}", udtg)
l = strings.ReplaceAll(l, "{Time}", nowTime)
l = strings.ReplaceAll(l, "{UTime}", nowTimeUTC)
l = strings.ReplaceAll(l, "{GPS}", gpsFmt(degreeMinute, nowPos))
l = strings.ReplaceAll(l, "{GPS_DECIMAL}", gpsFmt(decimal, nowPos))
l = strings.ReplaceAll(l, "{GPS_SIGNED_DECIMAL}", gpsFmt(signedDecimal, nowPos))
// Lots of undocumented tags found in the Winlink check in form.
n2ygk marked this conversation as resolved.
Show resolved Hide resolved
// Note also various ways of capitalizing. Perhaps best to do case insenstive string replacements....
l = strings.ReplaceAll(l, "{Latitude}", fmt.Sprintf("%.4f", nowPos.Lat))
l = strings.ReplaceAll(l, "{latitude}", fmt.Sprintf("%.4f", nowPos.Lat))
l = strings.ReplaceAll(l, "{Longitude}", fmt.Sprintf("%.4f", nowPos.Lon))
l = strings.ReplaceAll(l, "{longitude}", fmt.Sprintf("%.4f", nowPos.Lon))
l = strings.ReplaceAll(l, "{GridSquare}", posToGridSquare(nowPos))
l = strings.ReplaceAll(l, "{GPSValid}", fmt.Sprintf("%s ", validPos))
if placeholderRegEx != nil {
l = fillPlaceholders(l, placeholderRegEx, formVars)
}
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ func main() {
AppVersion: buildinfo.VersionStringShort(),
UserAgent: buildinfo.UserAgent(),
LineReader: readLine,
GPSd: config.GPSd,
})

// Make sure we clean up on exit, closing any open resources etc.
Expand Down