From 9a16f31a59360c89d012eab868ee00811df79a00 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sun, 13 Mar 2022 17:26:46 -0400 Subject: [PATCH] Add several (some undocumented) insertion tags that are required for the Winlink Check In form. --- internal/forms/forms.go | 125 ++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 2 files changed, 126 insertions(+) diff --git a/internal/forms/forms.go b/internal/forms/forms.go index 6a5a2255..3f24a311 100644 --- a/internal/forms/forms.go +++ b/internal/forms/forms.go @@ -17,6 +17,7 @@ import ( "io" "io/ioutil" "log" + "math" "net/http" "os" "path" @@ -30,6 +31,8 @@ import ( "unicode/utf8" "github.com/dimchansky/utfbom" + "github.com/la5nta/pat/cfg" + "github.com/la5nta/pat/internal/gpsd" ) const ( @@ -59,6 +62,7 @@ type Config struct { AppVersion string LineReader func() string UserAgent string + GPSd cfg.GPSdConfig } // Form holds information about a Winlink form template @@ -704,6 +708,108 @@ func (m *Manager) findAbsPathForTemplatePath(tmplPath string) (string, error) { return retVal, nil } +func (m *Manager) gpsPos() (gpsd.Position, error) { + // return current GPS Position + + addr := m.config.GPSd.Addr + if addr != "" { + conn, err := gpsd.Dial(addr) + if err != nil { + log.Printf("GPSd daemon: %s", err) + } + defer conn.Close() + + conn.Watch(true) + + log.Println("Waiting for position from GPSd...") + pos, err := conn.NextPosTimeout(3 * time.Second) // TODO: configurable + if err != nil { + log.Printf("GPSd: %s", err) + } else { + log.Printf("GPSd: %f %f", pos.Lat, pos.Lon) + } + return pos, err + } else { + return gpsd.Position{}, errors.New("GPSd: not configured.") + } +} + +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 { + var northing string + var easting string + var latDegrees int + var latMinutes float64 + var lonDegrees int + var 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 { + // function translated from Winlink_Check_In_Intial.html + var ( + U = "ABCDEFGHIJKLMNOPQRSTUVWX" + L = "abcdefghijklmnopqrstuvwx" + grid string + ) + adjLat := pos.Lat + 90.0 + adjLon := pos.Lon + 180.0 + grid = grid + string(U[int(adjLon/20.0)]) + string(U[int(adjLat/10.0)]) + nLat := int(adjLat) % 10 + nLon := int(adjLon/2) % 10 + grid = grid + fmt.Sprintf("%d%d", nLon, nLat) + rLat := (adjLat - math.Trunc(adjLat)) * 60.0 + rLon := (adjLon - 2*math.Trunc(adjLon/2)) * 60.0 + grid = grid + string(L[int(math.Trunc(rLon/5))]) + string(L[int(math.Trunc(rLat/2.5))]) + return grid +} + 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 { @@ -732,6 +838,13 @@ 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" + } else { + validPos = "YES" + } scanner := bufio.NewScanner(bytes.NewReader(sanitizedFileContent)) for scanner.Scan() { @@ -749,6 +862,18 @@ 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. + // 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)) + // l = strings.ReplaceAll(l, "{InternetAvailable}", "TO DO") if placeholderRegEx != nil { l = fillPlaceholders(l, placeholderRegEx, formVars) } diff --git a/main.go b/main.go index d3bd5625..2bcdf67a 100644 --- a/main.go +++ b/main.go @@ -325,6 +325,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.