417 lines
12 KiB
Go
417 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/adrianmo/go-nmea"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
/*
|
|
gpsd.go
|
|
|
|
A minimal gpsd like daemon which reads NMEA sentences and forms gpsd compatible records which are distributed
|
|
to TCP listeners, in particular to Pat.
|
|
|
|
Please note: this code does the bare minimum to supply a position received by a NMEA capable GNSS receiver
|
|
to Pat. It's main purpose it to avoid the need to run a separate gpsd for this particular purpose,
|
|
but it is ** by far ** no full-featured replacement for gpsd.
|
|
|
|
Torsten Harenberg, DL1THM. Feb 2025.
|
|
*/
|
|
|
|
var currentTPV TPV
|
|
|
|
var (
|
|
clients = make(map[net.Conn]struct{})
|
|
clientMutex sync.Mutex
|
|
)
|
|
|
|
type WatchMessage struct {
|
|
Class string `json:"class"`
|
|
Enable bool `json:"enable"`
|
|
JSON bool `json:"json"`
|
|
Nmea bool `json:"nmea"`
|
|
RAW int `json:"raw"`
|
|
Scaled bool `json:"scaled"`
|
|
Timing bool `json:"timing"`
|
|
Split24 bool `json:"split_24"`
|
|
PPS bool `json:"pps"`
|
|
}
|
|
|
|
// TPV repräsentiert ein GPSd TPV (Time-Position-Velocity) JSON-Objekt.
|
|
type TPV struct {
|
|
Class string `json:"class"` // immer "TPV"
|
|
Device string `json:"device,omitempty"` // z.B. "/dev/ttyS0"
|
|
Time string `json:"time,omitempty"` // Zeitangabe im RFC3339-Format
|
|
Lat float64 `json:"lat,omitempty"` // Breitengrad
|
|
Lon float64 `json:"lon,omitempty"` // Längengrad
|
|
Alt float64 `json:"alt,omitempty"` // Höhe (Meter)
|
|
Speed float64 `json:"speed"` // Geschwindigkeit (m/s)
|
|
Track float64 `json:"track,omitempty"` // Kurs (Grad)
|
|
Mode int `json:"mode,omitempty"` // Fix-Fodus aus GSA: 1 = kein Fix, 2 = 2D, 3 = 3D
|
|
PDOP float64 `json:"pdop,omitempty"` // Positions-DOP
|
|
HDOP float64 `json:"hdop,omitempty"` // Horizontaler DOP
|
|
VDOP float64 `json:"vdop,omitempty"` // Vertikaler DOP
|
|
}
|
|
|
|
// SKY repräsentiert ein GPSd SKY JSON-Objekt, das Satellitendaten enthält.
|
|
type SKY struct {
|
|
Class string `json:"class"` // immer "SKY"
|
|
Device string `json:"device,omitempty"` // z.B. "/dev/ttyS0"
|
|
Satellites []SatelliteInfo `json:"satellites,omitempty"`
|
|
}
|
|
|
|
// SatelliteInfo fasst Informationen zu einem einzelnen Satelliten zusammen.
|
|
type SatelliteInfo struct {
|
|
PRN int `json:"PRN"` // PRN/ID des Satelliten
|
|
Elevation int `json:"elevation"` // Höhe (Grad)
|
|
Azimuth int `json:"azimuth"` // Azimut (Grad)
|
|
SNR int `json:"ss"` // Signalstärke (in dB)
|
|
}
|
|
|
|
// ParseWatchMessage extrahiert und parst die ?WATCH Nachricht
|
|
func ParseWatchMessage(input string) (*WatchMessage, error) {
|
|
|
|
prefix := "?WATCH="
|
|
if !strings.HasPrefix(input, prefix) {
|
|
return nil, fmt.Errorf("ungültiges Format: muss mit %q beginnen", prefix)
|
|
}
|
|
|
|
jsonPart := input[len(prefix):]
|
|
|
|
var msg WatchMessage
|
|
if err := json.Unmarshal([]byte(jsonPart), &msg); err != nil {
|
|
return nil, fmt.Errorf("fehler beim Parsen des JSON: %w", err)
|
|
}
|
|
|
|
return &msg, nil
|
|
}
|
|
|
|
func startGPSdTCPServer(gpsdaddress string) {
|
|
listener, err := net.Listen("tcp", gpsdaddress)
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("Error starting TCP server: %v\n", err), 0)
|
|
return
|
|
}
|
|
defer func() {
|
|
err := listener.Close()
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
}()
|
|
writeDebug(fmt.Sprintf("TCP server started on port %s", gpsdaddress), 1)
|
|
|
|
for {
|
|
client, err := listener.Accept()
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("Error accepting client connection: %v\n", err), 0)
|
|
continue
|
|
}
|
|
writeDebug(fmt.Sprintf("New GPSd client connected: %v", client.RemoteAddr()), 0)
|
|
addClient(client)
|
|
}
|
|
}
|
|
|
|
// isNetConnClosedErr classifies errors to determine if the net.Conn is closed. From https://stackoverflow.com/questions/44974984/how-to-check-a-net-conn-is-closed
|
|
func isNetConnClosedErr(err error) bool {
|
|
switch {
|
|
case
|
|
errors.Is(err, net.ErrClosed),
|
|
errors.Is(err, io.EOF),
|
|
errors.Is(err, syscall.EPIPE):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
func addClient(client net.Conn) {
|
|
|
|
if s.Userconfig.NMEAPassthrough {
|
|
go func() {
|
|
defer func() {
|
|
err := client.Close()
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
removeClient(client)
|
|
writeDebug(fmt.Sprintf("GPSd Client disconnected: %v\n", client.RemoteAddr()), 0)
|
|
}()
|
|
rd := bufio.NewScanner(client)
|
|
for rd.Scan() {
|
|
writeDebug(fmt.Sprintf("GPSd Client received message: %v\n", rd.Text()), 0)
|
|
}
|
|
}()
|
|
|
|
} else {
|
|
_, err := client.Write([]byte("{\"class\":\"VERSION\",\"release\":\"3.25\",\"rev\":\"3.25\",\"proto_major\":3,\"proto_minor\":15}\n"))
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("Error writing to client: %v\n", err), 1)
|
|
}
|
|
|
|
go func() {
|
|
defer func() {
|
|
err := client.Close()
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
removeClient(client)
|
|
writeDebug(fmt.Sprintf("GPSd Client disconnected: %v\n", client.RemoteAddr()), 0)
|
|
}()
|
|
writeDebug(fmt.Sprintf("gpsd: starting conversation with %v\n", client.RemoteAddr()), 0)
|
|
rd := bufio.NewReader(client)
|
|
for {
|
|
time.Sleep(100 * time.Millisecond)
|
|
// looks like gpsd does not expect \n terminated lines so read what is there from the socket
|
|
err := client.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
buff := make([]byte, 1024)
|
|
n, err := rd.Read(buff)
|
|
if isNetConnClosedErr(err) {
|
|
// socket closed, end the goroutine
|
|
break
|
|
}
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("gpsd: error reading from client: %v\n", err), 0)
|
|
continue
|
|
}
|
|
line := string(buff[:n])
|
|
|
|
writeDebug(fmt.Sprintf("gpsd: Received from client: %v\n", line), 1)
|
|
msg, err := ParseWatchMessage(line)
|
|
if err == nil {
|
|
|
|
// fill devices list
|
|
dl := fmt.Sprintf("{\"class\":\"DEVICES\",\"devices\":[{\"class\":\"DEVICE\",\"path\":\"%s\",\"driver\":\"NMEA0183\",\"activated\":\"2025-02-12T14:34:56.027Z\",\"flags\":1,\"native\":0,\"bps\":115200,\"parity\":\"N\",\"stopbits\":1,\"cycle\":1.00}]}\n", currentTPV.Device)
|
|
_, err = client.Write([]byte(dl))
|
|
writeDebug(dl, 0)
|
|
|
|
//reply to WATCH command
|
|
resp := WatchMessage{Class: "WATCH", Enable: msg.Enable, JSON: true}
|
|
jsonresp, err := json.Marshal(resp)
|
|
if err != nil {
|
|
fmt.Println("Fehler:", err)
|
|
}
|
|
jsonresp = append(jsonresp, byte('\n'))
|
|
_, err = client.Write(jsonresp)
|
|
writeDebug(fmt.Sprintf("gpsd: answer to client: %s\n", jsonresp), 1)
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("gpsd: error writing to client: %v\n", err), 0)
|
|
}
|
|
|
|
//current TPV
|
|
jsonOut, err := json.Marshal(currentTPV)
|
|
jsonOut = append(jsonOut, byte('\n'))
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("gpsd: error serializing TPV object: %v", err), 0)
|
|
return
|
|
}
|
|
_, err = client.Write(jsonOut)
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("gpsd: error writing to client: %v\n", err), 0)
|
|
}
|
|
writeDebug(fmt.Sprintf("gpsd: answer to client: %s\n", string(jsonOut)), 1)
|
|
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
// register the client, so it gets updates from now on
|
|
clientMutex.Lock()
|
|
clients[client] = struct{}{}
|
|
clientMutex.Unlock()
|
|
}
|
|
|
|
func removeClient(client net.Conn) {
|
|
clientMutex.Lock()
|
|
delete(clients, client)
|
|
clientMutex.Unlock()
|
|
}
|
|
|
|
// publishTPV serialisiert den aktuellen TPV-Zustand als JSON und gibt ihn aus.
|
|
func publishTPV() {
|
|
jsonOut, err := json.Marshal(currentTPV)
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("error serializing TPV object: %v", err), 0)
|
|
return
|
|
}
|
|
broadcastToClients(string(jsonOut))
|
|
}
|
|
|
|
func readAndBroadcast() {
|
|
device := s.DeviceType
|
|
if s.Userconfig.NMEAPassthrough {
|
|
for {
|
|
nmeaSentence, err := s.GPSStream.DequeueOrWait()
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("Error dequeuing GPS sentence: %v\n", err), 0)
|
|
continue
|
|
}
|
|
broadcastToClients(nmeaSentence)
|
|
}
|
|
} else {
|
|
for {
|
|
nmeaSentence, err := s.GPSStream.DequeueOrWait()
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("Error dequeuing GPS sentence: %v\n", err), 0)
|
|
continue
|
|
}
|
|
writeDebug(fmt.Sprintf("gpsd: received NMEA: %s", nmeaSentence), 1)
|
|
//broadcastToClients(string(nmeaSentence) + "\n")
|
|
|
|
// NMEA-Satz parsen
|
|
sentence, err := nmea.Parse(nmeaSentence)
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("gpsd: error parsing NMEA sentence '%s': %v", nmeaSentence, err), 1)
|
|
continue
|
|
}
|
|
|
|
switch s := sentence.(type) {
|
|
|
|
// RMC (Mindestdaten: Zeit, Position, Kurs, Geschwindigkeit)
|
|
case nmea.RMC:
|
|
updateTPVFromRMC(s, device)
|
|
|
|
// GGA (Positions- und Höheninformation)
|
|
case nmea.GGA:
|
|
updateTPVFromGGA(s, device)
|
|
|
|
// VTG: Aktualisierung von Kurs und Geschwindigkeit.
|
|
case nmea.VTG:
|
|
updateTPVFromVTG(s)
|
|
|
|
// GSV (Satelliten in Sicht)
|
|
case nmea.GSV:
|
|
sats := make([]SatelliteInfo, 0, len(s.Info))
|
|
for _, sat := range s.Info {
|
|
sats = append(sats, SatelliteInfo{
|
|
PRN: int(sat.SVPRNNumber),
|
|
Elevation: int(sat.Elevation),
|
|
Azimuth: int(sat.Azimuth),
|
|
SNR: int(sat.SNR),
|
|
})
|
|
}
|
|
sky := SKY{
|
|
Class: "SKY",
|
|
Device: device,
|
|
Satellites: sats,
|
|
}
|
|
if jsonOut, err := json.Marshal(sky); err == nil {
|
|
broadcastToClients(string(jsonOut))
|
|
//fmt.Println(string(jsonOut))
|
|
} else {
|
|
writeDebug(fmt.Sprintf("gpsd: error serializing SKY object: %v", err), 1)
|
|
}
|
|
|
|
// GSA (z.B. GPGSA oder auch ohne Talker-ID)
|
|
case nmea.GSA:
|
|
updateTPVFromGSA(s)
|
|
|
|
default:
|
|
writeDebug(fmt.Sprintf("unsupported NMEA type: %T", s), 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseTime(gpstime string) string {
|
|
// Remove any extra milliseconds after 3 digits (if needed)
|
|
gpstime = strings.Split(gpstime, ".")[0] + ".000"
|
|
now := time.Now()
|
|
parsedTime, err := time.Parse("15:04:05.000", gpstime)
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("Error parsing time: %s", err.Error()), 1)
|
|
return now.Format(time.RFC3339)
|
|
}
|
|
// Combine today's date with the parsed time
|
|
finalTime := time.Date(
|
|
now.Year(), now.Month(), now.Day(),
|
|
parsedTime.Hour(), parsedTime.Minute(), parsedTime.Second(), parsedTime.Nanosecond(),
|
|
now.Location(),
|
|
)
|
|
// Format in RFC3339
|
|
rfc3339Time := finalTime.Format(time.RFC3339)
|
|
return rfc3339Time
|
|
}
|
|
|
|
func updateTPVFromRMC(s nmea.RMC, device string) {
|
|
const knotsToMs = 0.514444
|
|
currentTPV.Class = "TPV"
|
|
currentTPV.Device = device
|
|
currentTPV.Time = parseTime(s.Time.String())
|
|
currentTPV.Lat = s.Latitude
|
|
currentTPV.Lon = s.Longitude
|
|
currentTPV.Speed = s.Speed * knotsToMs
|
|
currentTPV.Track = s.Course
|
|
// Now publish currentTPV to gpsd
|
|
publishTPV()
|
|
}
|
|
|
|
func updateTPVFromGGA(s nmea.GGA, device string) {
|
|
currentTPV.Class = "TPV"
|
|
currentTPV.Device = device
|
|
currentTPV.Time = parseTime(s.Time.String())
|
|
currentTPV.Lat = s.Latitude
|
|
currentTPV.Lon = s.Longitude
|
|
currentTPV.Alt = s.Altitude
|
|
// Publish currentTPV to gpsd
|
|
publishTPV()
|
|
}
|
|
|
|
func updateTPVFromGSA(s nmea.GSA) {
|
|
// Update only the fields provided by GSA
|
|
fixtype, _ := strconv.Atoi(s.FixType)
|
|
currentTPV.Mode = fixtype
|
|
currentTPV.PDOP = s.PDOP
|
|
currentTPV.HDOP = s.HDOP
|
|
currentTPV.VDOP = s.VDOP
|
|
// Publish currentTPV to gpsd
|
|
publishTPV()
|
|
}
|
|
|
|
// updateTPVFromVTG aktualisiert den TPV-Zustand mit den in VTG verfügbaren Daten (Kurs und Geschwindigkeit).
|
|
func updateTPVFromVTG(s nmea.VTG) {
|
|
// Falls ein wahrer Kurs (TrackTrue) angegeben wurde, diesen übernehmen:
|
|
if s.TrueTrack != 0 {
|
|
currentTPV.Track = s.TrueTrack
|
|
}
|
|
// Geschwindigkeit: Wir rechnen die in Knoten angegebene Geschwindigkeit in m/s um.
|
|
if s.GroundSpeedKnots != 0 {
|
|
const knotsToMs = 0.514444
|
|
currentTPV.Speed = s.GroundSpeedKnots * knotsToMs
|
|
}
|
|
if s.GroundSpeedKPH != 0 {
|
|
const kphtoMs = 3.6
|
|
currentTPV.Speed = s.GroundSpeedKPH * kphtoMs
|
|
}
|
|
publishTPV()
|
|
}
|
|
|
|
func broadcastToClients(message string) {
|
|
clientMutex.Lock()
|
|
defer clientMutex.Unlock()
|
|
|
|
for client := range clients {
|
|
_, err := client.Write([]byte(message + "\n"))
|
|
if err != nil {
|
|
writeDebug(fmt.Sprintf("gpsd: error writing to client %v: %v\n", client.RemoteAddr(), err), 0)
|
|
err = client.Close()
|
|
if err != nil {
|
|
writeDebug(err.Error(), 0)
|
|
}
|
|
delete(clients, client)
|
|
}
|
|
writeDebug(fmt.Sprintf("gpsd: message: %v", message), 1)
|
|
}
|
|
}
|