Hands On

Reverse Geocoding NEO 6M GPS Positions with Golang and a Serial UART Connection

By Nic Raboy | 16 May 2019

A while back you might have come across a tutorial I wrote titled, Reverse Geocoding Coordinates to Addresses with the Go Programming Language. This tutorial was useful, but we used static data to represent our latitude and longitude positions. In a realistic scenario we probably want to get coordinate information from a GPS module or similar and then use that real-time data to make reverse geocoding requests.

I had a u-blox NEO 6M GPS module laying around that I was using with my Arduino and figured it would make a cool example if I connected it to my computer with a serial to USB cable for gathering position information.

In this tutorial we’re going to use the Go programming language to interact with our serial port, decode GPS data, and reverse geocode it for address estimates.

The Requirements

Because there is a hardware emphasis on this tutorial, there are a few requirements to be successful. You should consider purchasing the following:

I’m not saying other hardware won’t work in this tutorial, but the above is what I used. You don’t absolutely need the antenna, but without it, you could sit around for a day or so waiting for a GPS fix with the satellites. Also, be prepared to solder if your GPS module doesn’t have any pins attached for the debug cable.

Beyond the hardware requirements, you will need Golang installed and configured on your computer and you will need a HERE Developer Portal account for using the reverse geocoding service.

Interacting with the Serial Port using Golang

We’re going to be working with a three step process, the first of which is getting GPS data from the serial port. The data will not be clean, but we’ll fix it up as we progress.

To interact with the serial port, there are quite a few packages available. I had the most luck with the go-serial package. It can be installed by executing the following:

go get github.com/jacobsa/go-serial/serial

Within your $GOPATH create a new project with a main.go file in it. As part of our boilerplate code, add the following:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "time"

    "github.com/jacobsa/go-serial/serial"
)

func main() {
    fmt.Println("Starting the application...")
}

To interact with the serial port, it must be opened and then the stream of bytes that come from the open serial port must be processed. Let’s look at how that’d happen with Go.

Inside the main function, include the following:

func main() {
    fmt.Println("Starting the application...")
    options := serial.OpenOptions{
        PortName:        "/dev/cu.SLAB_USBtoUART",
        BaudRate:        9600,
        DataBits:        8,
        StopBits:        1,
        MinimumReadSize: 4,
    }
    serialPort, err := serial.Open(options)
    if err != nil {
        log.Fatalf("serial.Open: %v", err)
    }
    defer serialPort.Close()
    reader := bufio.NewReader(serialPort)
    scanner := bufio.NewScanner(reader)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

Notice that I’ve defined my actual serial port in the above code. Make sure you choose the correct port that represents your GPS module. After you open the port, you must read the byte data. This is where it could potentially get complicated. The byte data doesn’t consistently come in, but yet there is a true format that must be followed. This format says that each piece of data is separated by a new line. You could create your own buffers to process the bytes and look out for new line characters or you can use a Scanner where each line in it can be accessed with the Text function.

Depending on your GPS module, you’ll get different formatted data. My NEO 6M GPS spits out data that looks like the following:

$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47

The above is a NMEA sentence and it must be parsed to make sense of it. Note that if your GPS hasn’t gotten a satellite fix, your NMEA sentences will be incomplete.

Parsing the NMEA GPS Data into Something Useable

Parsing the NMEA sentences is not difficult, but some setup must happen first. I recommend creating another file within your project to separate your code.

Within your project, create a nmea.go file with the following:

package main

import (
    "errors"
    "fmt"
    "math"
    "strconv"
    "strings"
)

type NMEA struct {
    fixTimestamp       string
    latitude           string
    latitudeDirection  string
    longitude          string
    longitudeDirection string
    fixQuality         string
    satellites         string
    horizontalDilution string
    antennaAltitude    string
    antennaHeight      string
    updateAge          string
}

func ParseNMEALine(line string) (NMEA, error) { }
func ParseDegrees(value string, direction string) (string, error) { }
func (nmea NMEA) GetLatitude() (string, error) { }
func (nmea NMEA) GetLongitude() (string, error) { }

The NMEA sentence is essentially a comma delimited list, so we just need to load it into a Go data structure and format the data. To load it, take a look at the ParseNMEALine function:

func ParseNMEALine(line string) (NMEA, error) {
    tokens := strings.Split(line, ",")
    if tokens[0] == "$GPGGA" {
        return NMEA{
            fixTimestamp:       tokens[1],
            latitude:           tokens[2],
            latitudeDirection:  tokens[3],
            longitude:          tokens[4],
            longitudeDirection: tokens[5],
            fixQuality:         tokens[6],
            satellites:         tokens[7],
        }, nil
    }
    return NMEA{}, errors.New("unsupported nmea string")
}

In my example we are only parsing $GPGGA formatted sentences. There are quite a few other options. You’ll know which one you have because the sentence will start with the format. The line that is passed to this function comes from the Scanner from the previous step.

After we have our data in a Go data structure we can parse the latitude and longitude. Right now it will be in some kind of degrees, minutes, seconds (DMS) format, but we want a pure decimal version of it.

Take a look at the following ParseDegrees function:

func ParseDegrees(value string, direction string) (string, error) {
    if value == "" || direction == "" {
        return "", errors.New("the location and / or direction value does not exist")
    }
    lat, _ := strconv.ParseFloat(value, 64)
    degrees := math.Floor(lat / 100)
    minutes := ((lat / 100) - math.Floor(lat/100)) * 100 / 60
    decimal := degrees + minutes
    if direction == "W" || direction == "S" {
        decimal *= -1
    }
    return fmt.Sprintf("%.6f", decimal), nil
}

What we’re doing is separating the degrees from the minutes and applying some math. Let’s say we have 4322.0678 as the value that comes from our GPS. We are trying to do the following:

result = 43 + (22.0678 / 60)

In the above example, 43 is the degrees and 22.0678 is our minutes.

Depending on the direction or quadrant that is returned, North, South, West, East, we can decide whether our number should be negative or positive. To make use of this ParseDegrees function we can do the following:

func (nmea NMEA) GetLatitude() (string, error) {
    return ParseDegrees(nmea.latitude, nmea.latitudeDirection)
}

func (nmea NMEA) GetLongitude() (string, error) {
    return ParseDegrees(nmea.longitude, nmea.longitudeDirection)
}

So as of right now we are reading data from our serial port and we have some parsing mechanisms in place, but we are not actually parsing our data yet. We need to connect the two steps now.

Go back into the main.go file and add the following:

func main() {
    fmt.Println("Starting the application...")
    options := serial.OpenOptions{
        PortName:        "/dev/cu.SLAB_USBtoUART",
        BaudRate:        9600,
        DataBits:        8,
        StopBits:        1,
        MinimumReadSize: 4,
    }
    serialPort, err := serial.Open(options)
    if err != nil {
        log.Fatalf("serial.Open: %v", err)
    }
    defer serialPort.Close()
    reader := bufio.NewReader(serialPort)
    scanner := bufio.NewScanner(reader)
    for scanner.Scan() {
        gps, err := ParseNMEALine(scanner.Text())
        if err == nil {
            if gps.fixQuality == "1" || gps.fixQuality == "2" {
                latitude, _ := gps.GetLatitude()
                longitude, _ := gps.GetLongitude()
                fmt.Println(latitude + "," + longitude)
            } else {
                fmt.Println("no gps fix available")
            }
            time.Sleep(2 * time.Second)
        }
    }
}

So instead of just printing out our scanned line, we are now parsing it. If we don’t get any errors, we can check for the fix quality. The fix quality basically tells us whether or not our GPS has gotten a fix with the satellites. No fix will result in no valuable data. If we have a fix, we can get our parsed latitude and longitude and print it out.

Since data comes in quite fast, we can add a sleep delay to slow things down.

Getting an Address Estimate from the Latitude and Longitude GPS Coordinates

So by now we should have our parsed latitude and longitude data. This data, at least when I tried, was down to just a few meters in accuracy which is quite awesome. Since latitude and longitude data isn’t exactly human readable, we should probably reverse geocode it to get an estimated address.

Like I previously mentioned, I had written a tutorial strictly on reverse geocoding location data with Golang. We’re pretty much going to copy and paste from it.

Within your project, create a heredev.go file with the following code:

package main

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/url"
)

type GeocoderResponse struct {
    Response struct {
        MetaInfo struct {
            TimeStamp string `json:"TimeStamp"`
        } `json:"MetaInfo"`
        View []struct {
            Result []struct {
                MatchLevel string `json:"MatchLevel"`
                Location   struct {
                    Address struct {
                        Label       string `json:"Label"`
                        Country     string `json:"Country"`
                        State       string `json:"State"`
                        County      string `json:"County"`
                        City        string `json:"City"`
                        District    string `json:"District"`
                        Street      string `json:"Street"`
                        HouseNumber string `json:"HouseNumber"`
                        PostalCode  string `json:"PostalCode"`
                    } `json:"Address"`
                } `json:"Location"`
            } `json:"Result"`
        } `json:"View"`
    } `json:"Response"`
}

type Position struct {
    Latitude  string `json:"latitude"`
    Longitude string `json:"longitude"`
}

type Geocoder struct {
    AppId   string `json:"app_id"`
    AppCode string `json:"app_code"`
}

func (geocoder *Geocoder) reverse(position Position) (GeocoderResponse, error) {
    endpoint, _ := url.Parse("https://reverse.geocoder.api.here.com/6.2/reversegeocode.json")
    queryParams := endpoint.Query()
    queryParams.Set("app_id", geocoder.AppId)
    queryParams.Set("app_code", geocoder.AppCode)
    queryParams.Set("mode", "retrieveAddresses")
    queryParams.Set("prox", position.Latitude+","+position.Longitude)
    endpoint.RawQuery = queryParams.Encode()
    response, err := http.Get(endpoint.String())
    if err != nil {
        return GeocoderResponse{}, err
    } else {
        data, _ := ioutil.ReadAll(response.Body)
        var geocoderResponse GeocoderResponse
        json.Unmarshal(data, &geocoderResponse)
        return geocoderResponse, nil
    }
}

Most of the above code is just data structure definitions modeled around the response data that comes back from the API. We also have a reverse function which will make the HTTP request to the API.

If you want more information on the little bit that is happening above, I suggest you read the previous tutorial.

Now let’s head back into the main.go file and make use of it:

geocoder := Geocoder{AppId: "APP-ID-HERE", AppCode: "APP-CODE-HERE"}
for scanner.Scan() {
    gps, err := ParseNMEALine(scanner.Text())
    if err == nil {
        if gps.fixQuality == "1" || gps.fixQuality == "2" {
            latitude, _ := gps.GetLatitude()
            longitude, _ := gps.GetLongitude()
            fmt.Println(latitude + "," + longitude)
            result, _ := geocoder.reverse(Position{Latitude: latitude, Longitude: longitude})
            if len(result.Response.View) > 0 && len(result.Response.View[0].Result) > 0 {
                fmt.Println(result.Response.View[0].Result[0].Location.Address.Label)
            } else {
                fmt.Println("no address estimates found for the position")
            }
        } else {
            fmt.Println("no gps fix available")
        }
        time.Sleep(2 * time.Second)
    }
}

In the above code we’ve now added our HERE Developer Portal credentials and made a request using the latitude and longitude data that comes back from the GPS. If results come back from the reverse geocode attempt, we can look at the first result and print out the address.

Essentially what’s happening is we get a NMEA sentence from the GPS, we parse it, we print the latitude and longitude, we reverse geocode the latitude and longitude, and we print out the address. We do this for every sentence that comes back in our stream.

Conclusion

You just saw how to process GPS data with Golang using a serial port connection and the HERE Reverse Geocoding API. Like previously mentioned, while a diverse set of hardware might work with this example, I can only validate the hardware that I listed. I also strongly recommend you have an antenna for your GPS module otherwise you might be left scratching your head as to whether your GPS module is working or your code.