├── .gitignore ├── LICENSE ├── README.md ├── examples └── main.go ├── go.mod └── gpsd.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | examples/examples 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Josip Lisec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-gpsd 2 | 3 | *GPSD client for Go.* 4 | 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/stratoberry/go-gpsd.svg)](https://pkg.go.dev/github.com/stratoberry/go-gpsd) 6 | 7 | ## Installation 8 | 9 | ### Packages 10 | 11 | #### Linux 12 | - Debian unstable: `apt install golang-github-stratoberry-go-gpsd-dev` 13 | 14 | [![Packaging status](https://repology.org/badge/vertical-allrepos/go:github-stratoberry-go-gpsd.svg)](https://repology.org/project/go:github-stratoberry-go-gpsd/versions) 15 | 16 | From source 17 | ``` 18 | # go get github.com/stratoberry/go-gpsd 19 | ``` 20 | 21 | go-gpsd has no external dependencies. 22 | 23 | ## Usage 24 | 25 | go-gpsd is a streaming client for GPSD's JSON service and as such can be used only in async manner unlike clients for other languages which support both async and sync modes. 26 | 27 | ```go 28 | import ("github.com/stratoberry/go-gpsd") 29 | 30 | func main() { 31 | gps, err := gpsd.Dial("localhost:2947") 32 | if err != nil { panic(err) } 33 | } 34 | ``` 35 | 36 | After `Dial`ing the server, you should install stream filters. Stream filters allow you to capture only certain types of GPSD reports. 37 | 38 | ```go 39 | gps.AddFilter("TPV", tpvFilter) 40 | ``` 41 | 42 | Filter functions have a type of `gps.Filter` and should receive one argument of type `interface{}`. 43 | 44 | ```go 45 | tpvFilter := func(r interface{}) { 46 | report := r.(*gpsd.TPVReport) 47 | fmt.Println("Location updated", report.Lat, report.Lon) 48 | } 49 | ``` 50 | 51 | Due to the nature of GPSD reports your filter will manually have to cast the type of the argument it received to a proper `*gpsd.Report` struct pointer. 52 | 53 | After installing all needed filters, call the `Watch` method to start observing reports. Please note that at this time installed filters can't be removed. 54 | 55 | ```go 56 | done := gps.Watch() 57 | <-done 58 | // ...some time later... 59 | gps.Close() 60 | ``` 61 | 62 | `Watch()` spans a new goroutine in which all data processing will happen, `done` doesn't send anything. 63 | 64 | ### Currently supported GPSD report types 65 | 66 | * [`VERSION`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_version) (`gpsd.VERSIONReport`) 67 | * [`TPV`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_tpv) (`gpsd.TPVReport`) 68 | * [`SKY`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_sky) (`gpsd.SKYReport`) 69 | * [`ATT`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_att) (`gpsd.ATTReport`) 70 | * [`GST`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_gst) (`gpsd.GSTReport`) 71 | * [`PPS`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_pps) (`gpsd.PPSReport`) 72 | * [`TOFF`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_toff) (`gpsd.TOFFReport`) 73 | * [`DEVICES`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_devices) (`gpsd.DEVICESReport`) 74 | * [`DEVICE`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_device_device) (`gpsd.DEVICEReport`) 75 | * [`ERROR`](https://gpsd.gitlab.io/gpsd/gpsd_json.html#_error) (`gpsd.ERRORReport`) 76 | 77 | ## Documentation 78 | 79 | For complete library documentation visit [Go Reference](https://pkg.go.dev/github.com/stratoberry/go-gpsd). 80 | 81 | Documentation of GPSD's JSON protocol is available at [https://gpsd.gitlab.io/gpsd/gpsd_json.html](https://gpsd.gitlab.io/gpsd/gpsd_json.html). 82 | 83 | ## References 84 | 85 | This library was originally developed as a part of a student project at [FOI/University of Zagreb](https://www.foi.unizg.hr/en). 86 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | var gps *gpsd.Session 7 | var err error 8 | 9 | if gps, err = gpsd.Dial(gpsd.DefaultAddress); err != nil { 10 | panic(fmt.Sprintf("Failed to connect to GPSD: %s", err)) 11 | } 12 | 13 | gps.AddFilter("TPV", func(r interface{}) { 14 | tpv := r.(*gpsd.TPVReport) 15 | fmt.Println("TPV", tpv.Mode, tpv.Time) 16 | }) 17 | 18 | skyfilter := func(r interface{}) { 19 | sky := r.(*gpsd.SKYReport) 20 | 21 | fmt.Println("SKY", len(sky.Satellites), "satellites") 22 | } 23 | 24 | gps.AddFilter("SKY", skyfilter) 25 | 26 | done := gps.Watch() 27 | <-done 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stratoberry/go-gpsd 2 | 3 | go 1.18 -------------------------------------------------------------------------------- /gpsd.go: -------------------------------------------------------------------------------- 1 | package gpsd 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "time" 11 | ) 12 | 13 | // DefaultAddress of gpsd (localhost:2947) 14 | const DefaultAddress = "localhost:2947" 15 | 16 | // Filter is a gpsd entry filter function 17 | type Filter func(interface{}) 18 | 19 | // Session represents a connection to gpsd 20 | type Session struct { 21 | socket net.Conn 22 | reader *bufio.Reader 23 | filters map[string][]Filter 24 | } 25 | 26 | // Mode describes status of a TPV report 27 | type Mode byte 28 | 29 | const ( 30 | // NoValueSeen indicates no data has been received yet 31 | NoValueSeen Mode = 0 32 | // NoFix indicates fix has not been required yet 33 | NoFix Mode = 1 34 | // Mode2D represents quality of the fix 35 | Mode2D Mode = 2 36 | // Mode3D represents quality of the fix 37 | Mode3D Mode = 3 38 | ) 39 | 40 | type gpsdReport struct { 41 | Class string `json:"class"` 42 | } 43 | 44 | // TPVReport is a Time-Position-Velocity report 45 | type TPVReport struct { 46 | Class string `json:"class"` 47 | Tag string `json:"tag"` 48 | Device string `json:"device"` 49 | Mode Mode `json:"mode"` 50 | Time time.Time `json:"time"` 51 | Ept float64 `json:"ept"` 52 | Lat float64 `json:"lat"` 53 | Lon float64 `json:"lon"` 54 | Alt float64 `json:"alt"` 55 | Epx float64 `json:"epx"` 56 | Epy float64 `json:"epy"` 57 | Epv float64 `json:"epv"` 58 | Track float64 `json:"track"` 59 | Speed float64 `json:"speed"` 60 | Climb float64 `json:"climb"` 61 | Epd float64 `json:"epd"` 62 | Eps float64 `json:"eps"` 63 | Epc float64 `json:"epc"` 64 | Eph float64 `json:"eph"` 65 | } 66 | 67 | // SKYReport reports sky view of GPS satellites 68 | type SKYReport struct { 69 | Class string `json:"class"` 70 | Tag string `json:"tag"` 71 | Device string `json:"device"` 72 | Time time.Time `json:"time"` 73 | Xdop float64 `json:"xdop"` 74 | Ydop float64 `json:"ydop"` 75 | Vdop float64 `json:"vdop"` 76 | Tdop float64 `json:"tdop"` 77 | Hdop float64 `json:"hdop"` 78 | Pdop float64 `json:"pdop"` 79 | Gdop float64 `json:"gdop"` 80 | Satellites []Satellite `json:"satellites"` 81 | } 82 | 83 | // GSTReport is pseudorange noise report 84 | type GSTReport struct { 85 | Class string `json:"class"` 86 | Tag string `json:"tag"` 87 | Device string `json:"device"` 88 | Time time.Time `json:"time"` 89 | Rms float64 `json:"rms"` 90 | Major float64 `json:"major"` 91 | Minor float64 `json:"minor"` 92 | Orient float64 `json:"orient"` 93 | Lat float64 `json:"lat"` 94 | Lon float64 `json:"lon"` 95 | Alt float64 `json:"alt"` 96 | } 97 | 98 | // ATTReport reports vehicle-attitude from the digital compass or the gyroscope 99 | type ATTReport struct { 100 | Class string `json:"class"` 101 | Tag string `json:"tag"` 102 | Device string `json:"device"` 103 | Time time.Time `json:"time"` 104 | Heading float64 `json:"heading"` 105 | MagSt string `json:"mag_st"` 106 | Pitch float64 `json:"pitch"` 107 | PitchSt string `json:"pitch_st"` 108 | Yaw float64 `json:"yaw"` 109 | YawSt string `json:"yaw_st"` 110 | Roll float64 `json:"roll"` 111 | RollSt string `json:"roll_st"` 112 | Dip float64 `json:"dip"` 113 | MagLen float64 `json:"mag_len"` 114 | MagX float64 `json:"mag_x"` 115 | MagY float64 `json:"mag_y"` 116 | MagZ float64 `json:"mag_z"` 117 | AccLen float64 `json:"acc_len"` 118 | AccX float64 `json:"acc_x"` 119 | AccY float64 `json:"acc_y"` 120 | AccZ float64 `json:"acc_z"` 121 | GyroX float64 `json:"gyro_x"` 122 | GyroY float64 `json:"gyro_y"` 123 | Depth float64 `json:"depth"` 124 | Temperature float64 `json:"temperature"` 125 | } 126 | 127 | // VERSIONReport returns version details of gpsd client 128 | type VERSIONReport struct { 129 | Class string `json:"class"` 130 | Release string `json:"release"` 131 | Rev string `json:"rev"` 132 | ProtoMajor int `json:"proto_major"` 133 | ProtoMinor int `json:"proto_minor"` 134 | Remote string `json:"remote"` 135 | } 136 | 137 | // DEVICESReport lists all devices connected to the system 138 | type DEVICESReport struct { 139 | Class string `json:"class"` 140 | Devices []DEVICEReport `json:"devices"` 141 | Remote string `json:"remote"` 142 | } 143 | 144 | // DEVICEReport reports a state of a particular device 145 | type DEVICEReport struct { 146 | Class string `json:"class"` 147 | Path string `json:"path"` 148 | Activated string `json:"activated"` 149 | Flags int `json:"flags"` 150 | Driver string `json:"driver"` 151 | Subtype string `json:"subtype"` 152 | Bps int `json:"bps"` 153 | Parity string `json:"parity"` 154 | Stopbits string `json:"stopbits"` 155 | Native int `json:"native"` 156 | Cycle float64 `json:"cycle"` 157 | Mincycle float64 `json:"mincycle"` 158 | } 159 | 160 | // PPSReport is triggered on each pulse-per-second strobe from a device 161 | type PPSReport struct { 162 | Class string `json:"class"` 163 | Device string `json:"device"` 164 | RealSec float64 `json:"real_sec"` 165 | RealMusec float64 `json:"real_musec"` 166 | ClockSec float64 `json:"clock_sec"` 167 | ClockMusec float64 `json:"clock_musec"` 168 | } 169 | 170 | // TOFFReport is triggered on each PPS strobe from a device 171 | type TOFFReport struct { 172 | Class string `json:"class"` 173 | Device string `json:"device"` 174 | RealSec float64 `json:"real_sec"` 175 | RealNSec float64 `json:"real_nsec"` 176 | ClockSec float64 `json:"clock_sec"` 177 | ClockNSec float64 `json:"clock_nsec"` 178 | } 179 | 180 | // ERRORReport is an error response 181 | type ERRORReport struct { 182 | Class string `json:"class"` 183 | Message string `json:"message"` 184 | } 185 | 186 | // Satellite describes a location of a GPS satellite 187 | type Satellite struct { 188 | PRN float64 `json:"PRN"` 189 | Az float64 `json:"az"` 190 | El float64 `json:"el"` 191 | Ss float64 `json:"ss"` 192 | Used bool `json:"used"` 193 | GnssId float64 `json:"gnssid"` 194 | SvId float64 `json:"svid"` 195 | Health float64 `json:"health"` 196 | } 197 | 198 | // Dial opens a new connection to GPSD. 199 | func Dial(address string) (*Session, error) { 200 | return dialCommon(net.Dial("tcp4", address)) 201 | } 202 | 203 | // DialTimeout opens a new connection to GPSD with a timeout. 204 | func DialTimeout(address string, to time.Duration) (*Session, error) { 205 | return dialCommon(net.DialTimeout("tcp4", address, to)) 206 | } 207 | 208 | func dialCommon(c net.Conn, err error) (session *Session, e error) { 209 | session = new(Session) 210 | session.socket = c 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | session.reader = bufio.NewReader(session.socket) 216 | session.reader.ReadString('\n') 217 | session.filters = make(map[string][]Filter) 218 | 219 | return 220 | } 221 | 222 | // Watch starts watching GPSD reports in a new goroutine. 223 | // 224 | // Example: 225 | // 226 | // gps := gpsd.Dial(gpsd.DEFAULT_ADDRESS) 227 | // done := gpsd.Watch() 228 | // <- done 229 | func (s *Session) Watch() (done chan bool) { 230 | fmt.Fprintf(s.socket, "?WATCH={\"enable\":true,\"json\":true}") 231 | done = make(chan bool) 232 | 233 | go watch(done, s) 234 | 235 | return 236 | } 237 | 238 | // SendCommand sends a command to GPSD 239 | func (s *Session) SendCommand(command string) { 240 | fmt.Fprintf(s.socket, "?"+command+";") 241 | } 242 | 243 | // AddFilter attaches a function which will be called for all 244 | // GPSD reports with the given class. Callback functions have type Filter. 245 | // 246 | // Example: 247 | // 248 | // gps := gpsd.Init(gpsd.DEFAULT_ADDRESS) 249 | // gps.AddFilter("TPV", func (r interface{}) { 250 | // report := r.(*gpsd.TPVReport) 251 | // fmt.Println(report.Time, report.Lat, report.Lon) 252 | // }) 253 | // done := gps.Watch() 254 | // <- done 255 | func (s *Session) AddFilter(class string, f Filter) { 256 | s.filters[class] = append(s.filters[class], f) 257 | } 258 | 259 | func (s *Session) deliverReport(class string, report interface{}) { 260 | for _, f := range s.filters[class] { 261 | f(report) 262 | } 263 | } 264 | 265 | // Close closes the connection to GPSD 266 | func (s *Session) Close() error { 267 | if s.socket == nil { 268 | return errors.New("gpsd socket is alerady closed") 269 | } 270 | 271 | if err := s.socket.Close(); err != nil { 272 | return err 273 | } 274 | 275 | s.socket = nil 276 | return nil 277 | } 278 | 279 | func watch(done chan bool, s *Session) { 280 | // We're not using a JSON decoder because we first need to inspect 281 | // the JSON string to determine it's "class" 282 | for { 283 | if line, err := s.reader.ReadString('\n'); err == nil { 284 | var reportPeek gpsdReport 285 | lineBytes := []byte(line) 286 | if err = json.Unmarshal(lineBytes, &reportPeek); err == nil { 287 | if len(s.filters[reportPeek.Class]) == 0 { 288 | continue 289 | } 290 | 291 | if report, err2 := unmarshalReport(reportPeek.Class, lineBytes); err2 == nil { 292 | s.deliverReport(reportPeek.Class, report) 293 | } else { 294 | fmt.Println("JSON parsing error 2:", err) 295 | } 296 | } else { 297 | fmt.Println("JSON parsing error:", err) 298 | } 299 | } else { 300 | if !errors.Is(err, net.ErrClosed) { 301 | fmt.Println("Stream reader error (is gpsd running?):", err) 302 | } 303 | if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { 304 | break 305 | } 306 | } 307 | } 308 | done <- true 309 | } 310 | 311 | func unmarshalReport(class string, bytes []byte) (interface{}, error) { 312 | var err error 313 | 314 | switch class { 315 | case "TPV": 316 | var r *TPVReport 317 | err = json.Unmarshal(bytes, &r) 318 | return r, err 319 | case "SKY": 320 | var r *SKYReport 321 | err = json.Unmarshal(bytes, &r) 322 | return r, err 323 | case "GST": 324 | var r *GSTReport 325 | err = json.Unmarshal(bytes, &r) 326 | return r, err 327 | case "ATT": 328 | var r *ATTReport 329 | err = json.Unmarshal(bytes, &r) 330 | return r, err 331 | case "VERSION": 332 | var r *VERSIONReport 333 | err = json.Unmarshal(bytes, &r) 334 | return r, err 335 | case "DEVICES": 336 | var r *DEVICESReport 337 | err = json.Unmarshal(bytes, &r) 338 | return r, err 339 | case "PPS": 340 | var r *PPSReport 341 | err = json.Unmarshal(bytes, &r) 342 | return r, err 343 | case "TOFF": 344 | var r *TOFFReport 345 | err = json.Unmarshal(bytes, &r) 346 | return r, err 347 | case "ERROR": 348 | var r *ERRORReport 349 | err = json.Unmarshal(bytes, &r) 350 | return r, err 351 | } 352 | 353 | return nil, err 354 | } 355 | --------------------------------------------------------------------------------