From ec18e60869948b063b605b8c3825b5388d5fc0f0 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 27 Jan 2024 13:53:15 -0800 Subject: [PATCH] updating tools --- cmd/ft8grid/main.go | 45 +++++++++++ cmd/gridsq/main.go | 186 +++++++++++++++++++++++++++++++++++++++++++ cmd/kepfetch/main.go | 12 +++ ft8/ft8.go | 161 +++++++++++++++++++++++++++++++++++++ ft8/ft8_test.go | 18 +++++ go.mod | 1 + go.sum | 2 + main.go | 89 --------------------- 8 files changed, 425 insertions(+), 89 deletions(-) create mode 100644 cmd/ft8grid/main.go create mode 100644 cmd/gridsq/main.go create mode 100644 cmd/kepfetch/main.go create mode 100644 ft8/ft8.go create mode 100644 ft8/ft8_test.go delete mode 100644 main.go diff --git a/cmd/ft8grid/main.go b/cmd/ft8grid/main.go new file mode 100644 index 0000000..0f66329 --- /dev/null +++ b/cmd/ft8grid/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "flag" + "fmt" + "git.wntrmute.dev/kyle/goutils/die" + "git.wntrmute.dev/kyle/gridsq/ft8" + "path/filepath" +) + +func main() { + defaultDir := ft8.GetDefaultDirectory() + defaultLog := filepath.Join(defaultDir, "ALL.TXT") + + var logFile string + flag.StringVar(&logFile, "f", defaultLog, "path to logfile") + flag.Parse() + + if flag.NArg() == 0 { + die.With("No callsign specified!") + } + + records, err := ft8.ProcessFile(logFile) + die.If(err) + + grids := map[string]bool{} + who := flag.Arg(0) + fmt.Println("find grid for ", who) + + for _, rec := range records { + if !rec.HasGrid() { + continue + } + + if rec.De != who { + continue + } + + if grids[rec.Grid] { + continue + } + grids[rec.Grid] = true + fmt.Printf("%s: %s\n", rec.De, rec.Grid) + } +} diff --git a/cmd/gridsq/main.go b/cmd/gridsq/main.go new file mode 100644 index 0000000..9c15570 --- /dev/null +++ b/cmd/gridsq/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "regexp" + "strconv" + "strings" + + "git.wntrmute.dev/kyle/goutils/die" + "github.com/klaus-tockloth/coco" + "github.com/logocomune/maidenhead" + geo "gopkg.in/billups/golang-geo.v2" +) + +var qths = map[int]string{ + 4: "CM97", + 6: "CM97cq", + 8: "CM97cq53", + 10: "CM97cq53CI", +} + +var mgrsAccuracyToMaidenheadDigits = map[int]int{ + 1: 10, + 10: 10, + 100: 10, + 1000: 8, + 10000: 6, +} + +var kmToMile = 0.621371 + +func cToP(c maidenhead.Coordinate) *geo.Point { + return geo.NewPoint(c.Lat, c.Lng) +} + +func distBearing(qth, qsl string) (err error) { + var cqth, cqsl maidenhead.Coordinate + cqth.Lat, cqth.Lng, err = maidenhead.GridCenter(qth) + if err != nil { + return err + } + + cqsl.Lat, cqsl.Lng, err = maidenhead.GridCenter(qsl) + if err != nil { + return err + } + + pqth := cToP(cqth) + pqsl := cToP(cqsl) + dist := pqth.GreatCircleDistance(pqsl) + bearing := pqth.BearingTo(pqsl) + fmt.Printf("%s => %s: distance %0.2f miles, bearing %0.1f°\n", + qth, qsl, dist*kmToMile, bearing) + return nil +} + +var utmZoneRegexp = regexp.MustCompile(`^(\d+)(\w+)$`) +var utmNorthingRegexp = regexp.MustCompile(`^(\d+)\w*[nsNS]$`) +var utmEastingRegexp = regexp.MustCompile(`^(\d+)\w+[ewEW]$`) +var errInvalidUTM = errors.New("invalid UTM coordinate") + +func stringToUTM(s string) coco.UTM { + utm := coco.UTM{ + ZoneNumber: 0, + ZoneLetter: 0, + Easting: 0, + Northing: 0, + } + + fields := strings.Fields(s) + if len(fields) != 3 { + die.If(errInvalidUTM) + } + + zone := utmZoneRegexp.FindStringSubmatch(fields[0]) + if len(zone) != 3 { + fmt.Println(zone) + die.If(errInvalidUTM) + } + + var err error + utm.ZoneNumber, err = strconv.Atoi(zone[1]) + if err != nil { + die.If(errors.Join(err, errInvalidUTM)) + } + utm.ZoneLetter = zone[2][0] + + easting := utmEastingRegexp.FindStringSubmatch(fields[1]) + if len(easting) == 0 { + die.If(errInvalidUTM) + } + utm.Easting, err = strconv.ParseFloat(easting[1], 64) + if err != nil { + die.If(errors.Join(err, errInvalidUTM)) + } + + northing := utmNorthingRegexp.FindStringSubmatch(fields[2]) + if len(northing) == 0 { + die.If(errInvalidUTM) + } + utm.Northing, err = strconv.ParseFloat(northing[1], 64) + if err != nil { + die.If(errors.Join(err, errInvalidUTM)) + } + + return utm +} + +func main() { + var lat, lon float64 + var precision int64 + + flag.Float64Var(&lat, "lat", 0.0, "latitude of grid") + flag.Float64Var(&lon, "lon", 0.0, "longitude of grid") + flag.Int64Var(&precision, "d", 6, "number of digits") + flag.Parse() + + if flag.NArg() == 0 { + fmt.Println("nothing") + return + } + + cmd := flag.Arg(0) + switch cmd { + case "geo": + grid, err := maidenhead.Locator(lat, lon, int(precision)) + die.If(err) + + fmt.Printf("%0.6f %0.6f: ", lat, lon) + fmt.Println(grid) + case "qth": + grid, ok := qths[int(precision)] + if !ok { + fmt.Println(qths[6]) + } else { + fmt.Println(grid) + } + case "db": + var qth, qsl string + if flag.NArg() == 2 { + qsl = flag.Arg(1) + qth = qths[len(qsl)] + } else { + qsl = flag.Arg(2) + qth = flag.Arg(1) + } + + err := distBearing(qth, qsl) + die.If(err) + + case "mgrs": + if flag.NArg() == 0 { + + return + } + + utm := coco.MGRS(strings.Replace(flag.Arg(1), " ", "", -1)) + ll, accuracy, err := utm.ToLL() + fmt.Printf("%#v\n", ll) + die.If(err) + + precision, ok := mgrsAccuracyToMaidenheadDigits[accuracy] + if !ok { + precision = 4 + } + grid, err := maidenhead.Locator(ll.Lat, ll.Lon, precision) + die.If(err) + fmt.Println(grid) + case "utm": + if flag.NArg() == 0 { + + return + } + + utm := stringToUTM(flag.Arg(1)) + ll, err := utm.ToLL() + fmt.Printf("%#v\n", ll) + die.If(err) + + grid, err := maidenhead.Locator(ll.Lat, ll.Lon, 8) + die.If(err) + fmt.Println(grid) + } +} diff --git a/cmd/kepfetch/main.go b/cmd/kepfetch/main.go new file mode 100644 index 0000000..5a6070f --- /dev/null +++ b/cmd/kepfetch/main.go @@ -0,0 +1,12 @@ +package main + +type Kep struct { + Filename string + URL string +} + +type User struct { + Name string + Directory string + Prefix string +} diff --git a/ft8/ft8.go b/ft8/ft8.go new file mode 100644 index 0000000..02fde51 --- /dev/null +++ b/ft8/ft8.go @@ -0,0 +1,161 @@ +package ft8 + +import ( + "bufio" + "git.wntrmute.dev/kyle/goutils/die" + "os" + "os/user" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "time" +) + +func getHomeDirectory() string { + u, err := user.Current() + if err != nil { + return "" + } + + return u.HomeDir +} + +func GetDefaultDirectory() string { + switch runtime.GOOS { + case "linux": + return filepath.Join( + getHomeDirectory(), + ".local", + "share", + "WSJT-X", + ) + case "windows": + return filepath.Join( + getHomeDirectory(), + "AppData", + "Local", + "WSJT-X", + ) + default: + die.With("Operating system '%s' isn't supported.", runtime.GOOS) + } + + return "" +} + +const ( + RecordTypeCQ = iota + 1 + RecordTypeCQReply + RecordTypeSignal + RecordTypeSignoff +) + +// call ([\w\d/]+) +// grid (\w{2}\d{2}) +var ( + RegexpCQ = regexp.MustCompile(`^CQ ([\w/]+) (\w{2}\d{2})$`) + RegexpCQReply = regexp.MustCompile(`^([\w/]+) ([\w/]+) (\w{2}\d{2})$`) + RegexpRR73 = regexp.MustCompile(`^([\w\d/]+) ([\w\d/]+) RR73$`) +) + +type Record struct { + Type uint8 + Time time.Time + Freq float64 + Tone int + Mode string + IsTX bool + De string + To string + Received int + Grid string +} + +func (rec *Record) HasGrid() bool { + return rec.Grid != "" +} + +func ParseRecordHeader(line string) (rec *Record, err error) { + // 0 1 2 3 4 5 6 7 + // 231215_021330 14.074 Rx FT8 -6 0.1 1713 CQ N6ACA CM97 + fields := strings.Fields(line) + rec = &Record{} + rec.Time, err = time.Parse("060102_150405", fields[0]) + if err != nil { + return + } + + rec.Freq, err = strconv.ParseFloat(fields[1], 64) + if err != nil { + return + } + + if fields[2] == "Tx" { + rec.IsTX = true + } + + rec.Mode = fields[3] + rec.Received, err = strconv.Atoi(fields[4]) + if err != nil { + return + } + + rec.Tone, err = strconv.Atoi(fields[6]) + if err != nil { + return + } + return +} + +func ParseRecord(line string) (*Record, error) { + rec, err := ParseRecordHeader(line) + if err != nil { + return nil, err + } + + fields := strings.Fields(line) + line = strings.Join(fields[7:len(fields)], " ") + + switch { + case RegexpRR73.MatchString(line): + rec.Type = RecordTypeSignoff + match := RegexpRR73.FindStringSubmatch(line) + rec.De = match[2] + rec.To = match[1] + case RegexpCQ.MatchString(line): + rec.Type = RecordTypeCQ + match := RegexpCQ.FindStringSubmatch(line) + rec.De = match[1] + rec.Grid = match[2] + case RegexpCQReply.MatchString(line): + rec.Type = RecordTypeCQReply + match := RegexpCQReply.FindStringSubmatch(line) + rec.To = match[1] + rec.De = match[2] + rec.Grid = match[3] + } + return rec, err +} + +func ProcessFile(path string) ([]*Record, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + var records []*Record + scanner := bufio.NewScanner(file) + for scanner.Scan() { + rec, err := ParseRecord(scanner.Text()) + if err != nil { + return nil, err + } + + records = append(records, rec) + } + + return records, nil +} diff --git a/ft8/ft8_test.go b/ft8/ft8_test.go new file mode 100644 index 0000000..231de69 --- /dev/null +++ b/ft8/ft8_test.go @@ -0,0 +1,18 @@ +package ft8 + +import "testing" + +var ( + testCQLine = `231215_021330 14.074 Rx FT8 -6 0.1 1713 CQ N6ACA CM97` +) + +func TestMatchCQ(t *testing.T) { + rec, err := ParseRecord(testCQLine) + if err != nil { + t.Fatal(err) + } + + if rec.Type != RecordTypeCQ { + t.Fatalf("invalid record: expected %d, have %d", RecordTypeCQ, rec.Type) + } +} diff --git a/go.mod b/go.mod index 3c0c1ad..ed57070 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21.3 require ( git.wntrmute.dev/kyle/goutils v1.7.4 // indirect + github.com/klaus-tockloth/coco v0.2.0 // indirect github.com/logocomune/maidenhead v1.0.1 // indirect gopkg.in/billups/golang-geo.v2 v2.0.0-20170124000346-c2931833be19 // indirect ) diff --git a/go.sum b/go.sum index 0124620..94a20f3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ git.wntrmute.dev/kyle/goutils v1.7.4 h1:kbvUoxRwAEemz4jL52AUKaOipuCX8F8PGTQHS5V3lRY= git.wntrmute.dev/kyle/goutils v1.7.4/go.mod h1:1PGn83Ac98KWyI6yfpCVyP1Ji61PX6lFpROxY+IoTJg= +github.com/klaus-tockloth/coco v0.2.0 h1:k2GWaR+dbWi/d9EIgNhSqmEshpTJSoWLGoCmPd559/Y= +github.com/klaus-tockloth/coco v0.2.0/go.mod h1:ZKByCKTYUR9wceWX7RUQmL1bDc0Q2SkbVfgrNs2UyIw= github.com/logocomune/maidenhead v1.0.1 h1:WnBpC/LIhc81QnwDdxEPKHwSAspMCWwsYMuM9vQMFgA= github.com/logocomune/maidenhead v1.0.1/go.mod h1:FLkUKuGyo4uKESNlXG8jgQgaTSApyR2ZZfq7m2Btmjs= gopkg.in/billups/golang-geo.v2 v2.0.0-20170124000346-c2931833be19 h1:vu0Y9rNRRTFG70fnQ/W/CG4N629vz9uaqaY7R7fXnVo= diff --git a/main.go b/main.go deleted file mode 100644 index ebc748f..0000000 --- a/main.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "flag" - "fmt" - - "git.wntrmute.dev/kyle/goutils/die" - "github.com/logocomune/maidenhead" - geo "gopkg.in/billups/golang-geo.v2" -) - -var qths = map[int]string{ - 4: "CM97", - 6: "CM97cq", - 8: "CM97cq53", - 10: "CM97cq53CI", -} - -var kmToMile = 0.621371 - -func cToP(c maidenhead.Coordinate) *geo.Point { - return geo.NewPoint(c.Lat, c.Lng) -} - -func distBearing(qth, qsl string) (err error) { - var cqth, cqsl maidenhead.Coordinate - cqth.Lat, cqth.Lng, err = maidenhead.GridCenter(qth) - if err != nil { - return err - } - - cqsl.Lat, cqsl.Lng, err = maidenhead.GridCenter(qsl) - if err != nil { - return err - } - - pqth := cToP(cqth) - pqsl := cToP(cqsl) - dist := pqth.GreatCircleDistance(pqsl) - bearing := pqth.BearingTo(pqsl) - fmt.Printf("%s => %s: distance %0.2f miles, bearing %0.1f°\n", - qth, qsl, dist*kmToMile, bearing) - return nil -} - -func main() { - var lat, lon float64 - var precision int64 - - flag.Float64Var(&lat, "lat", 0.0, "latitude of grid") - flag.Float64Var(&lon, "lon", 0.0, "longitude of grid") - flag.Int64Var(&precision, "d", 6, "number of digits") - flag.Parse() - - if flag.NArg() == 0 { - fmt.Println("nothing") - return - } - - cmd := flag.Arg(0) - switch cmd { - case "geo": - grid, err := maidenhead.Locator(lat, lon, int(precision)) - die.If(err) - - fmt.Printf("%0.6f %0.6f: ", lat, lon) - fmt.Println(grid) - case "qth": - grid, ok := qths[int(precision)] - if !ok { - fmt.Println(qths[6]) - } else { - fmt.Println(grid) - } - case "db": - var qth, qsl string - if flag.NArg() == 2 { - qsl = flag.Arg(1) - qth = qths[len(qsl)] - } else { - qsl = flag.Arg(2) - qth = flag.Arg(1) - } - - err := distBearing(qth, qsl) - die.If(err) - } - -}