diff --git a/Makefile b/Makefile index a99657f..8de82df 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ BUILD_FOLDER=build BINARY=${BUILD_FOLDER}/nmea_ugps # Pass variables for version number, sha id and build number -VERSION=1.3.0 +VERSION=1.4.0 SHA=$(shell git rev-parse --short HEAD) # Set fallback build num if not set by environment variable BUILDNUM?=local diff --git a/README.md b/README.md index 234a7d4..1339920 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ The application also reads the latitude/longitude of the Locator from the Underw The application is run on the command line and can be configured via arguments. The arguments are: ``` + -d debug + -heading string + Input sentence type to use for heading. Supported: HDM, HDT, THS (default "HDT") -i string UDP device and port (host:port) OR serial device (COM7 /dev/ttyUSB1@4800) to listen for NMEA input. (default "0.0.0.0:7777") -o string @@ -33,16 +36,16 @@ The application is run on the command line and can be configured via arguments. URL of Underwater GPS (default "http://192.168.2.94") ``` -Example using UART input from /dev/ttyUSB2 with baud rate 4800 and sending the output via UDP on port 9999 on localhost. +Example using UART input from /dev/ttyUSB2 with baud rate 4800 using THS sentence for heading and sending the output via UDP on port 2947 on localhost. ``` -./nmea_ugps_linux_amd64 -i /dev/ttyUSB2@4800 -o 127.0.0.1:9999 +./nmea_ugps_linux_amd64 -i /dev/ttyUSB2@4800 -o 127.0.0.1:2947 -heading THS ``` On Windows, the easiest is to create a `start.bat` file, edit it with notepad to the desired settings and then double-click it in Explorer to start it. Example of what the file can look like: ``` -nmea_ugps_windows_amd64.exe -i COM1 -o 127.0.0.1:2947 +nmea_ugps_windows_amd64.exe -i COM1@4800 -o 127.0.0.1:2947 -heading HDM pause ``` @@ -58,9 +61,9 @@ When running the application it typically looks like this: └──────────────────────────────────────────────────────────────────────────────┘ ┌─Input status─────────────────────────────────────────────────────────────────┐ │Supported NMEA sentences received: │ -│ * GGA: 5 │ -│ * HDT: 6 │ -│ * THS: 0 │ +│ * Position : GGA: 5 │ +│ * Heading : HDT: 6 │ +│ * Parse error: 0 │ │Sent sucessfully to UGPS: 10 │ │ │ │ │ diff --git a/in.go b/in.go index 06335d7..1e8ab3d 100644 --- a/in.go +++ b/in.go @@ -9,18 +9,18 @@ import ( "strings" "time" - "github.com/waterlinked/go-nmea" "github.com/tarm/serial" + "github.com/waterlinked/go-nmea" ) type inputStats struct { - typeGga int - typeHdt int - typeHdm int - typeThs int - isErr bool - errorMsg string - sendOk int + posDesc string + posCount int + headDesc string + unparsableCount int + isErr bool + errorMsg string + sendOk int } const missingDataTimeout = 10 @@ -31,21 +31,23 @@ var ( ) // parseNMEA takes a string and return true if new data, else false -func parseNMEA(data []byte) (bool, error) { +func parseNMEA(data []byte, h headingParser) (bool, error) { line := strings.TrimSpace(string(data)) s, err := nmea.Parse(line) if err != nil { - return false, err + debugPrintf("Parse err: %s (%s)", err, line) + stats.unparsableCount++ + return false, nil } switch m := s.(type) { case nmea.GPGGA: - //debugPrintf("GGA: Lat/lon : %s %s\n", nmea.FormatGPS(m.Latitude), nmea.FormatGPS(m.Longitude)) + debugPrintf("GGA: Lat/lon : %s %s\n", nmea.FormatGPS(m.Latitude), nmea.FormatGPS(m.Longitude)) fix, err := strconv.ParseFloat(m.FixQuality, 64) if err != nil { - log.Printf("GGA invalid fix quality: %s -> %v\n", m.FixQuality, err) + debugPrintf("GGA invalid fix quality: %s -> %v\n", m.FixQuality, err) fix = 0 } latest.Lat = m.Latitude @@ -53,29 +55,17 @@ func parseNMEA(data []byte) (bool, error) { latest.NumSats = float64(m.NumSatellites) latest.FixQuality = fix latest.Hdop = m.HDOP - stats.typeGga++ - return true, nil - - case nmea.GPHDT: - //debugPrintf("HDT: Heading : %f\n", m.Heading) - latest.Orientation = m.Heading - stats.typeHdt++ - return true, nil - case nmea.HDM: - //debugPrintf("HDM: Heading : %f\n", m.Heading) - latest.Orientation = m.Heading - stats.typeHdm++ - return true, nil - case nmea.THS: - //debugPrintf("THS: Heading : %f\n", m.Heading) - latest.Orientation = m.Heading - stats.typeThs++ + //stats.typeGga++ + stats.posCount++ + stats.posDesc = fmt.Sprintf("GGA: %d", stats.posCount) return true, nil } - return false, nil + success, err := h.parseNMEA(s) + stats.headDesc = h.String() + return success, err } -func inputUDPLoop(listen string, msg chan externalMaster, inStatsCh chan inputStats) { +func inputUDPLoop(listen string, hParser headingParser, msg chan externalMaster, inStatsCh chan inputStats) { udpAddr, err := net.ResolveUDPAddr("udp4", listen) if err != nil { log.Fatal(err) @@ -105,7 +95,7 @@ func inputUDPLoop(listen string, msg chan externalMaster, inStatsCh chan inputSt continue } - gotUpdate, err := parseNMEA(buffer[:n]) + gotUpdate, err := parseNMEA(buffer[:n], hParser) if err != nil { stats.errorMsg = fmt.Sprintf("%v", err) stats.isErr = true @@ -120,7 +110,7 @@ func inputUDPLoop(listen string, msg chan externalMaster, inStatsCh chan inputSt } } -func inputSerialLoop(s *serial.Port, msg chan externalMaster, inStatsCh chan inputStats) { +func inputSerialLoop(s *serial.Port, hParser headingParser, msg chan externalMaster, inStatsCh chan inputStats) { scanner := bufio.NewReader(s) for { @@ -131,7 +121,7 @@ func inputSerialLoop(s *serial.Port, msg chan externalMaster, inStatsCh chan inp inStatsCh <- stats continue } - gotUpdate, err := parseNMEA(line) + gotUpdate, err := parseNMEA(line, hParser) if err != nil { stats.errorMsg = fmt.Sprintf("%v", err) @@ -160,6 +150,7 @@ func inputLoop(masterCh chan externalMaster, inputStatusCh chan inputStats) { if err == nil { stats.sendOk++ } else { + debugPrintf("%v", err) stats.isErr = true stats.errorMsg = fmt.Sprintf("%v", err) inputStatusCh <- stats diff --git a/in_parser.go b/in_parser.go new file mode 100644 index 0000000..7193a4e --- /dev/null +++ b/in_parser.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + + "github.com/waterlinked/go-nmea" +) + +// headingParser is the interface parsing heading input +type headingParser interface { + // parseNMEA takes a nmea.Sentence and return true if new data, else false + parseNMEA(sentence nmea.Sentence) (bool, error) + // String returns a string representing the current status + String() string +} + +type hdmParser struct { + count int +} +type hdtParser struct { + count int +} +type thsParser struct { + count int +} + +func (p *hdmParser) parseNMEA(sentence nmea.Sentence) (bool, error) { + switch m := sentence.(type) { + case nmea.HDM: + debugPrintf("HDM: Heading : %f\n", m.Heading) + latest.Orientation = m.Heading + p.count++ + return true, nil + } + return false, nil +} + +func (p hdmParser) String() string { + return fmt.Sprintf("HDM: %d", p.count) +} + +func (p *hdtParser) parseNMEA(sentence nmea.Sentence) (bool, error) { + switch m := sentence.(type) { + case nmea.GPHDT: + debugPrintf("HDT: Heading : %f\n", m.Heading) + latest.Orientation = m.Heading + p.count++ + return true, nil + } + return false, nil +} + +func (p hdtParser) String() string { + return fmt.Sprintf("HDT: %d", p.count) +} + +func (p *thsParser) parseNMEA(sentence nmea.Sentence) (bool, error) { + switch m := sentence.(type) { + case nmea.THS: + debugPrintf("THS: Heading : %f\n", m.Heading) + latest.Orientation = m.Heading + p.count++ + return true, nil + } + return false, nil +} + +func (p thsParser) String() string { + return fmt.Sprintf("THS: %d", p.count) +} diff --git a/main.go b/main.go index f851cbc..6e69060 100644 --- a/main.go +++ b/main.go @@ -17,19 +17,29 @@ import ( ) var ( - listen string - output string - sentence string - verbose bool + listen string + headingSentence string + output string + sentence string + debug bool Version string = "0.0.0" BuildNum string = "local" SHA string = "local" ) +const dbgLen = 5 + +var dbgMsg []string = make([]string, 0) + func debugPrintf(arguments string, a ...interface{}) { - if verbose { - log.Printf(arguments, a...) + if debug { + if len(dbgMsg) > dbgLen { + dbgMsg = dbgMsg[1:dbgLen] + } + s := time.Now().Format("15:04:05") + " " + fmt.Sprintf(arguments, a...) + dbgMsg = append(dbgMsg, strings.TrimSpace(s)) + //log.Printf(arguments, a...) } } @@ -55,28 +65,26 @@ func baudAndPortFromDevice(device string) (string, int) { return port, baudrate } -func keysForMap(m map[string]outSerializer) string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - joined := strings.Join(keys, ", ") - return joined -} - func main() { // Mapping sentence names to what serializer to use availableSerializers := make(map[string]outSerializer) availableSerializers["RATLL"] = tllSerializer{} availableSerializers["GPGGA"] = ggaSerializer{} - supportedSentences := keysForMap(availableSerializers) + supportedSentences := keysForMapS(availableSerializers) + + availableHeadingSentences := make(map[string]headingParser) + availableHeadingSentences["HDM"] = &hdmParser{} + availableHeadingSentences["HDT"] = &hdtParser{} + availableHeadingSentences["THS"] = &thsParser{} + supportedHeadings := keysForMapP(availableHeadingSentences) fmt.Printf("Water Linked NMEA UGPS bridge (v%s %s.%s)\n", Version, BuildNum, SHA) flag.StringVar(&listen, "i", "0.0.0.0:7777", "UDP device and port (host:port) OR serial device (COM7 /dev/ttyUSB1@4800) to listen for NMEA input. ") flag.StringVar(&output, "o", "", "UDP device and port (host:port) OR serial device (COM7 /dev/ttyUSB1) to send NMEA output. ") flag.StringVar(&sentence, "sentence", "GPGGA", "NMEA output sentence to use. Supported: "+supportedSentences) + flag.StringVar(&headingSentence, "heading", "HDT", "Input sentence type to use for heading. Supported: "+supportedHeadings) flag.StringVar(&baseURL, "url", "http://192.168.2.94", "URL of Underwater GPS") - //flag.BoolVar(&verbose, "v", false, "verbose") + flag.BoolVar(&debug, "d", false, "debug") flag.Parse() // Same serial port for input and output? @@ -85,9 +93,15 @@ func main() { fmt.Println("Same port for input and output", listen) } - serializer, exists := availableSerializers[sentence] + serializer, exists := availableSerializers[strings.ToUpper(sentence)] if !exists { - fmt.Printf("Unsupported setence '%s'. Supported are: %s\n", sentence, supportedSentences) + fmt.Printf("Unsupported sentence '%s'. Supported are: %s\n", sentence, supportedSentences) + os.Exit(1) + } + + hParser, exists := availableHeadingSentences[strings.ToUpper(headingSentence)] + if !exists { + fmt.Printf("Unsupported heading sentence '%s'. Supported are: %s\n", headingSentence, supportedHeadings) os.Exit(1) } @@ -102,7 +116,7 @@ func main() { // Setup input if deviceIsUDP(listen) { // Input from UDP - go inputUDPLoop(listen, masterCh, inStatusCh) + go inputUDPLoop(listen, hParser, masterCh, inStatusCh) } else { // Input from serial port port, baudrate := baudAndPortFromDevice(listen) @@ -115,7 +129,7 @@ func main() { } defer s.Close() - go inputSerialLoop(s, masterCh, inStatusCh) + go inputSerialLoop(s, hParser, masterCh, inStatusCh) if sameInOut { // Output is to same serial port as input writer = s @@ -165,6 +179,7 @@ func Bool2Int(val bool) int { return 0 } +// RunUI updates the GUI func RunUI(inStatusCh chan inputStats, outStatusCh chan outStats) { // Let the goroutines initialize before starting GUI time.Sleep(50 * time.Millisecond) @@ -175,11 +190,12 @@ func RunUI(inStatusCh chan inputStats, outStatusCh chan outStats) { y := 0 height := 5 + width := 80 p := widgets.NewParagraph() p.Title = "Water Linked Underwater GPS NMEA bridge" p.Text = fmt.Sprintf("PRESS q TO QUIT\nIn : %s\nOut: %s", listen, output) - p.SetRect(0, y, 80, height) + p.SetRect(0, y, width, height) y += height p.TextStyle.Fg = ui.ColorWhite p.BorderStyle.Fg = ui.ColorCyan @@ -188,7 +204,7 @@ func RunUI(inStatusCh chan inputStats, outStatusCh chan outStats) { inpStatus.Title = "Input status" inpStatus.Text = "Waiting for data" height = 12 - inpStatus.SetRect(0, y, 80, y+height) + inpStatus.SetRect(0, y, width, y+height) y += height inpStatus.TextStyle.Fg = ui.ColorGreen inpStatus.BorderStyle.Fg = ui.ColorCyan @@ -200,13 +216,26 @@ func RunUI(inStatusCh chan inputStats, outStatusCh chan outStats) { outStatus.Text = "Output not enabled" } height = 10 - outStatus.SetRect(0, y, 80, y+height) + outStatus.SetRect(0, y, width, y+height) y += height outStatus.TextStyle.Fg = ui.ColorGreen outStatus.BorderStyle.Fg = ui.ColorCyan + height = 15 + dbgText := widgets.NewList() + dbgText.Title = "Debug" + dbgText.Rows = dbgMsg + dbgText.WrapText = true + dbgText.SetRect(0, y, width, y+height) + y += height + //dbgText.TextStyle.Fg = ui.ColorGreen + dbgText.BorderStyle.Fg = ui.ColorCyan + draw := func() { ui.Render(p, inpStatus, outStatus) + if debug { + ui.Render(dbgText) + } } // Intial draw before any events have occured @@ -218,15 +247,19 @@ func RunUI(inStatusCh chan inputStats, outStatusCh chan outStats) { select { case instats := <-inStatusCh: inpStatus.TextStyle.Fg = ui.ColorGreen - inpStatus.Text = fmt.Sprintf("Supported NMEA sentences received:\n * GGA: %d\n * HDT: %d\n * HDM: %d\n * THS: %d\nSent sucessfully to UGPS: %d", - instats.typeGga, instats.typeHdt, instats.typeHdm, instats.typeThs, instats.sendOk) - numberOfHeadings := Bool2Int(instats.typeHdt > 0) + Bool2Int(instats.typeThs > 0) + Bool2Int(instats.typeHdm > 0) - if numberOfHeadings > 1 { - inpStatus.Text += "\nWarning: Multiple headings received, should have only 1. This will probably give jumpy positioning." - } + inpStatus.Text = fmt.Sprintf( + "Supported NMEA sentences received:\n"+ + " * Position : %s\n"+ + " * Heading : %s\n"+ + " * Parse error: %d\n"+ + "Sent sucessfully to UGPS: %d\n\n"+ + "%s", + instats.posDesc, instats.headDesc, instats.unparsableCount, instats.sendOk, instats.errorMsg) if instats.isErr { inpStatus.TextStyle.Fg = ui.ColorRed - inpStatus.Text += fmt.Sprintf("\n\n%s", instats.errorMsg) + } + if debug { + dbgText.Rows = dbgMsg } draw() case outstats := <-outStatusCh: @@ -237,6 +270,9 @@ func RunUI(inStatusCh chan inputStats, outStatusCh chan outStats) { outStatus.TextStyle.Fg = ui.ColorRed outStatus.Text += fmt.Sprintf("\n\n%v (%d)", outstats.errMsg, outstats.getErr) } + if debug { + dbgText.Rows = dbgMsg + } draw() case e := <-uiEvents: switch e.ID { @@ -246,3 +282,21 @@ func RunUI(inStatusCh chan inputStats, outStatusCh chan outStats) { } } } + +func keysForMapS(m map[string]outSerializer) string { + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + joined := strings.Join(keys, ", ") + return joined +} + +func keysForMapP(m map[string]headingParser) string { + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + joined := strings.Join(keys, ", ") + return joined +} diff --git a/out.go b/out.go index ee404a4..be980d3 100644 --- a/out.go +++ b/out.go @@ -31,6 +31,7 @@ func outputLoop(writer io.Writer, outStatusCh chan outStats, ser outSerializer) if err != nil { stats.isErr = true stats.errMsg = fmt.Sprintf("ERR get position from UGPS: %v", err) + debugPrintf(stats.errMsg) stats.getErr++ outStatusCh <- stats @@ -56,6 +57,7 @@ func outputLoop(writer io.Writer, outStatusCh chan outStats, ser outSerializer) if err != nil { stats.isErr = true stats.errMsg = fmt.Sprintf("NMEA out: %v", err) + debugPrintf(stats.errMsg) } else { stats.isErr = false stats.sendOk++ diff --git a/test/test-run.sh b/test/test-run.sh index 62543dd..c68bf5a 100755 --- a/test/test-run.sh +++ b/test/test-run.sh @@ -1,3 +1,3 @@ #!/bin/bash -go run *.go -url http://127.0.0.1:8080 +go run . -url http://127.0.0.1:8080 diff --git a/test/test-udp-send.sh b/test/test-udp-send.sh index 502fea7..3830e40 100755 --- a/test/test-udp-send.sh +++ b/test/test-udp-send.sh @@ -7,32 +7,38 @@ function send { } +function slumber { + #sleep 1 + sleep 0.1 +} + function once { send "\$bogous,,*47" + slumber send "\$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47" - sleep 2 + slumber send "\$GPHDT,274.07,T*03" - sleep 1 + slumber - sleep 2 send "\$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47" + slumber - sleep 1 send "\$GPHDT,274.07,T*03" #send "\$HCHDM,277.19,M*13" + slumber - sleep 1 #send "\$GPTHS,338.01,A*36" send "\$GPTHS,338.01,A*0E" + slumber - sleep 1 #send "\$GPTHS,338.01,A*36" send "\$HCHDM,276.71,M*1C" + slumber - sleep 1 send "\$GPHDT,274.07,T*03" + slumber } @@ -42,5 +48,5 @@ function once { while [[ 1 ]]; do once - sleep 1 + slumber done