kopia lustrzana https://github.com/cyoung/stratux
				
				
				
			
		
			
				
	
	
		
			1596 wiersze
		
	
	
		
			53 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			1596 wiersze
		
	
	
		
			53 KiB
		
	
	
	
		
			Go
		
	
	
| /*
 | |
| 	Copyright (c) 2015-2016 Christopher Young
 | |
| 	Distributable under the terms of The "BSD New"" License
 | |
| 	that can be found in the LICENSE file, herein included
 | |
| 	as part of this header.
 | |
| 
 | |
| 	ry835ai.go: GPS functions, GPS init, AHRS status messages, other external sensor monitoring.
 | |
| */
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"log"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"bufio"
 | |
| 
 | |
| 	"github.com/kidoman/embd"
 | |
| 	_ "github.com/kidoman/embd/host/all"
 | |
| 	"github.com/kidoman/embd/sensor/bmp180"
 | |
| 	"github.com/tarm/serial"
 | |
| 
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 
 | |
| 	"../mpu6050"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	SAT_TYPE_UNKNOWN = 0  // default type
 | |
| 	SAT_TYPE_GPS     = 1  // GPxxx; NMEA IDs 1-32
 | |
| 	SAT_TYPE_GLONASS = 2  // GLxxx; NMEA IDs 65-88
 | |
| 	SAT_TYPE_GALILEO = 3  // GAxxx; NMEA IDs unknown
 | |
| 	SAT_TYPE_BEIDOU  = 4  // GBxxx; NMEA IDs 201-235
 | |
| 	SAT_TYPE_SBAS    = 10 // NMEA IDs 33-54
 | |
| )
 | |
| 
 | |
| type SatelliteInfo struct {
 | |
| 	SatelliteNMEA    uint8     // NMEA ID of the satellite. 1-32 is GPS, 33-54 is SBAS, 65-88 is Glonass.
 | |
| 	SatelliteID      string    // Formatted code indicating source and PRN code. e.g. S138==WAAS satellite 138, G2==GPS satellites 2
 | |
| 	Elevation        int16     // Angle above local horizon, -xx to +90
 | |
| 	Azimuth          int16     // Bearing (degrees true), 0-359
 | |
| 	Signal           int8      // Signal strength, 0 - 99; -99 indicates no reception
 | |
| 	Type             uint8     // Type of satellite (GPS, GLONASS, Galileo, SBAS)
 | |
| 	TimeLastSolution time.Time // Time (system ticker) a solution was last calculated using this satellite
 | |
| 	TimeLastSeen     time.Time // Time (system ticker) a signal was last received from this satellite
 | |
| 	TimeLastTracked  time.Time // Time (system ticker) this satellite was tracked (almanac data)
 | |
| 	InSolution       bool      // True if satellite is used in the position solution (reported by GSA message or PUBX,03)
 | |
| }
 | |
| 
 | |
| type SituationData struct {
 | |
| 	mu_GPS *sync.Mutex
 | |
| 
 | |
| 	// From GPS.
 | |
| 	LastFixSinceMidnightUTC  float32
 | |
| 	Lat                      float32
 | |
| 	Lng                      float32
 | |
| 	Quality                  uint8
 | |
| 	HeightAboveEllipsoid     float32 // GPS height above WGS84 ellipsoid, ft. This is specified by the GDL90 protocol, but most EFBs use MSL altitude instead. HAE is about 70-100 ft below GPS MSL altitude over most of the US.
 | |
| 	GeoidSep                 float32 // geoid separation, ft, MSL minus HAE (used in altitude calculation)
 | |
| 	Satellites               uint16  // satellites used in solution
 | |
| 	SatellitesTracked        uint16  // satellites tracked (almanac data received)
 | |
| 	SatellitesSeen           uint16  // satellites seen (signal received)
 | |
| 	Accuracy                 float32 // 95% confidence for horizontal position, meters.
 | |
| 	NACp                     uint8   // NACp categories are defined in AC 20-165A
 | |
| 	Alt                      float32 // Feet MSL
 | |
| 	AccuracyVert             float32 // 95% confidence for vertical position, meters
 | |
| 	GPSVertVel               float32 // GPS vertical velocity, feet per second
 | |
| 	LastFixLocalTime         time.Time
 | |
| 	TrueCourse               float32
 | |
| 	GroundSpeed              uint16
 | |
| 	LastGroundTrackTime      time.Time
 | |
| 	GPSTime                  time.Time
 | |
| 	LastGPSTimeTime          time.Time // stratuxClock time since last GPS time received.
 | |
| 	LastValidNMEAMessageTime time.Time // time valid NMEA message last seen
 | |
| 	LastValidNMEAMessage     string    // last NMEA message processed.
 | |
| 
 | |
| 	mu_Attitude *sync.Mutex
 | |
| 
 | |
| 	// From BMP180 pressure sensor.
 | |
| 	Temp              float64
 | |
| 	Pressure_alt      float64
 | |
| 	LastTempPressTime time.Time
 | |
| 
 | |
| 	// From MPU6050 accel/gyro.
 | |
| 	Pitch            float64
 | |
| 	Roll             float64
 | |
| 	Gyro_heading     float64
 | |
| 	LastAttitudeTime time.Time
 | |
| }
 | |
| 
 | |
| var serialConfig *serial.Config
 | |
| var serialPort *serial.Port
 | |
| 
 | |
| var readyToInitGPS bool // TO-DO: replace with channel control to terminate goroutine when complete
 | |
| 
 | |
| var satelliteMutex *sync.Mutex
 | |
| var Satellites map[string]SatelliteInfo
 | |
| 
 | |
| /*
 | |
| u-blox5_Referenzmanual.pdf
 | |
| Platform settings
 | |
| Airborne <2g Recommended for typical airborne environment. No 2D position fixes supported.
 | |
| p.91 - CFG-MSG
 | |
| Navigation/Measurement Rate Settings
 | |
| Header 0xB5 0x62
 | |
| ID 0x06 0x08
 | |
| 0x0064 (100 ms)
 | |
| 0x0001
 | |
| 0x0001 (GPS time)
 | |
| {0xB5, 0x62, 0x06, 0x08, 0x00, 0x64, 0x00, 0x01, 0x00, 0x01}
 | |
| p.109 CFG-NAV5 (0x06 0x24)
 | |
| Poll Navigation Engine Settings
 | |
| */
 | |
| 
 | |
| func chksumUBX(msg []byte) []byte {
 | |
| 	ret := make([]byte, 2)
 | |
| 	for i := 0; i < len(msg); i++ {
 | |
| 		ret[0] = ret[0] + msg[i]
 | |
| 		ret[1] = ret[1] + ret[0]
 | |
| 	}
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| // p.62
 | |
| func makeUBXCFG(class, id byte, msglen uint16, msg []byte) []byte {
 | |
| 	ret := make([]byte, 6)
 | |
| 	ret[0] = 0xB5
 | |
| 	ret[1] = 0x62
 | |
| 	ret[2] = class
 | |
| 	ret[3] = id
 | |
| 	ret[4] = byte(msglen & 0xFF)
 | |
| 	ret[5] = byte((msglen >> 8) & 0xFF)
 | |
| 	ret = append(ret, msg...)
 | |
| 	chk := chksumUBX(ret[2:])
 | |
| 	ret = append(ret, chk[0])
 | |
| 	ret = append(ret, chk[1])
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| func makeNMEACmd(cmd string) []byte {
 | |
| 	chk_sum := byte(0)
 | |
| 	for i := range cmd {
 | |
| 		chk_sum = chk_sum ^ byte(cmd[i])
 | |
| 	}
 | |
| 	return []byte(fmt.Sprintf("$%s*%02x\x0d\x0a", cmd, chk_sum))
 | |
| }
 | |
| 
 | |
| func initGPSSerial() bool {
 | |
| 	var device string
 | |
| 	baudrate := int(9600)
 | |
| 	isSirfIV := bool(false)
 | |
| 
 | |
| 	if _, err := os.Stat("/dev/ublox8"); err == nil { // u-blox 8 (RY83xAI over USB).
 | |
| 		device = "/dev/ublox8"
 | |
| 	} else if _, err := os.Stat("/dev/ublox7"); err == nil { // u-blox 7 (VK-172, RY725AI over USB).
 | |
| 		device = "/dev/ublox7"
 | |
| 	} else if _, err := os.Stat("/dev/ublox6"); err == nil { // u-blox 6 (VK-162).
 | |
| 		device = "/dev/ublox6"
 | |
| 	} else if _, err := os.Stat("/dev/prolific0"); err == nil { // Assume it's a BU-353-S4 SIRF IV.
 | |
| 		//TODO: Check a "serialout" flag and/or deal with multiple prolific devices.
 | |
| 		isSirfIV = true
 | |
| 		baudrate = 4800
 | |
| 		device = "/dev/prolific0"
 | |
| 	} else if _, err := os.Stat("/dev/ttyAMA0"); err == nil { // ttyAMA0 is PL011 UART (GPIO pins 8 and 10) on all RPi.
 | |
| 		device = "/dev/ttyAMA0"
 | |
| 	} else {
 | |
| 		log.Printf("No suitable device found.\n")
 | |
| 		return false
 | |
| 	}
 | |
| 	if globalSettings.DEBUG {
 | |
| 		log.Printf("Using %s for GPS\n", device)
 | |
| 	}
 | |
| 
 | |
| 	/* Developer option -- uncomment to allow "hot" configuration of GPS (assuming 38.4 kpbs on warm start)
 | |
| 		serialConfig = &serial.Config{Name: device, Baud: 38400}
 | |
| 		p, err := serial.OpenPort(serialConfig)
 | |
| 		if err != nil {
 | |
| 			log.Printf("serial port err: %s\n", err.Error())
 | |
| 			return false
 | |
| 		} else { // reset port to 9600 baud for configuration
 | |
| 		        cfg1 := make([]byte, 20)
 | |
| 		        cfg1[0] = 0x01 // portID.
 | |
| 		        cfg1[1] = 0x00 // res0.
 | |
| 		        cfg1[2] = 0x00 // res1.
 | |
| 		        cfg1[3] = 0x00 // res1.
 | |
| 
 | |
| 	        	//      [   7   ] [   6   ] [   5   ] [   4   ]
 | |
| 		        //      0000 0000 0000 0000 1000 0000 1100 0000
 | |
| 		        // UART mode. 0 stop bits, no parity, 8 data bits. Little endian order.
 | |
| 		        cfg1[4] = 0xC0
 | |
| 		        cfg1[5] = 0x08
 | |
| 		        cfg1[6] = 0x00
 | |
| 		        cfg1[7] = 0x00
 | |
| 
 | |
| 	        	// Baud rate. Little endian order.
 | |
| 		        bdrt1 := uint32(9600)
 | |
| 		        cfg1[11] = byte((bdrt1 >> 24) & 0xFF)
 | |
| 		        cfg1[10] = byte((bdrt1 >> 16) & 0xFF)
 | |
| 		        cfg1[9] = byte((bdrt1 >> 8) & 0xFF)
 | |
| 		        cfg1[8] = byte(bdrt1 & 0xFF)
 | |
| 
 | |
| 	        	// inProtoMask. NMEA and UBX. Little endian.
 | |
| 		        cfg1[12] = 0x03
 | |
| 		        cfg1[13] = 0x00
 | |
| 
 | |
| 		        // outProtoMask. NMEA. Little endian.
 | |
| 		        cfg1[14] = 0x02
 | |
| 		        cfg1[15] = 0x00
 | |
| 
 | |
| 	        	cfg1[16] = 0x00 // flags.
 | |
| 		        cfg1[17] = 0x00 // flags.
 | |
| 
 | |
| 	        	cfg1[18] = 0x00 //pad.
 | |
| 		        cfg1[19] = 0x00 //pad.
 | |
| 
 | |
| 	        	p.Write(makeUBXCFG(0x06, 0x00, 20, cfg1))
 | |
| 			p.Close()
 | |
| 		}
 | |
| 
 | |
| 		-- End developer option */
 | |
| 
 | |
| 	// Open port at default baud for config.
 | |
| 	serialConfig = &serial.Config{Name: device, Baud: baudrate}
 | |
| 	p, err := serial.OpenPort(serialConfig)
 | |
| 	if err != nil {
 | |
| 		log.Printf("serial port err: %s\n", err.Error())
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	if isSirfIV {
 | |
| 		log.Printf("Using SiRFIV config.\n")
 | |
| 		// Enable 38400 baud.
 | |
| 		p.Write(makeNMEACmd("PSRF100,1,38400,8,1,0"))
 | |
| 		baudrate = 38400
 | |
| 		p.Close()
 | |
| 
 | |
| 		time.Sleep(250 * time.Millisecond)
 | |
| 		// Re-open port at newly configured baud so we can configure 5Hz messages.
 | |
| 		serialConfig = &serial.Config{Name: device, Baud: baudrate}
 | |
| 		p, err = serial.OpenPort(serialConfig)
 | |
| 
 | |
| 		// Enable 5Hz. (To switch back to 1Hz: $PSRF103,00,7,00,0*22)
 | |
| 		p.Write(makeNMEACmd("PSRF103,00,6,00,0"))
 | |
| 
 | |
| 		// Enable GGA.
 | |
| 		p.Write(makeNMEACmd("PSRF103,00,00,01,01"))
 | |
| 		// Enable GSA.
 | |
| 		p.Write(makeNMEACmd("PSRF103,02,00,01,01"))
 | |
| 		// Enable RMC.
 | |
| 		p.Write(makeNMEACmd("PSRF103,04,00,01,01"))
 | |
| 		// Enable VTG.
 | |
| 		p.Write(makeNMEACmd("PSRF103,05,00,01,01"))
 | |
| 		// Enable GSV (once every 5 position updates)
 | |
| 		p.Write(makeNMEACmd("PSRF103,03,00,05,01"))
 | |
| 
 | |
| 		if globalSettings.DEBUG {
 | |
| 			log.Printf("Finished writing SiRF GPS config to %s. Opening port to test connection.\n", device)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// Set 5 Hz update. Little endian order.
 | |
| 		//p.Write(makeUBXCFG(0x06, 0x08, 6, []byte{0x64, 0x00, 0x01, 0x00, 0x01, 0x00})) // 10 Hz
 | |
| 		p.Write(makeUBXCFG(0x06, 0x08, 6, []byte{0xc8, 0x00, 0x01, 0x00, 0x01, 0x00})) // 5 Hz
 | |
| 
 | |
| 		// Set navigation settings.
 | |
| 		nav := make([]byte, 36)
 | |
| 		nav[0] = 0x05 // Set dyn and fixMode only.
 | |
| 		nav[1] = 0x00
 | |
| 		// dyn.
 | |
| 		nav[2] = 0x07 // "Airborne with >2g Acceleration".
 | |
| 		nav[3] = 0x02 // 3D only.
 | |
| 
 | |
| 		p.Write(makeUBXCFG(0x06, 0x24, 36, nav))
 | |
| 
 | |
| 		// GNSS configuration CFG-GNSS for ublox 7 higher, p. 125 (v8)
 | |
| 		// NOTE: Max position rate = 5 Hz if GPS+GLONASS used.
 | |
| 
 | |
| 		// TESTING: 5Hz unified GPS + GLONASS
 | |
| 
 | |
| 		// Disable GLONASS to enable 10 Hz solution rate. GLONASS is not used
 | |
| 		// for SBAS (WAAS), so little real-world impact.
 | |
| 
 | |
| 		cfgGnss := []byte{0x00, 0x20, 0x20, 0x05}
 | |
| 		gps := []byte{0x00, 0x08, 0x10, 0x00, 0x01, 0x00, 0x01, 0x01}  // enable GPS with 8-16 tracking channels
 | |
| 		sbas := []byte{0x01, 0x02, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01} // enable SBAS (WAAS) with 2-3 tracking channels
 | |
| 		beidou := []byte{0x03, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x01}
 | |
| 		qzss := []byte{0x05, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x01}
 | |
| 		//glonass := []byte{0x06, 0x04, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x01} // this disables GLONASS
 | |
| 		glonass := []byte{0x06, 0x08, 0x0E, 0x00, 0x01, 0x00, 0x01, 0x01} // this enables GLONASS with 8-14 tracking channels
 | |
| 		cfgGnss = append(cfgGnss, gps...)
 | |
| 		cfgGnss = append(cfgGnss, sbas...)
 | |
| 		cfgGnss = append(cfgGnss, beidou...)
 | |
| 		cfgGnss = append(cfgGnss, qzss...)
 | |
| 		cfgGnss = append(cfgGnss, glonass...)
 | |
| 		p.Write(makeUBXCFG(0x06, 0x3E, uint16(len(cfgGnss)), cfgGnss))
 | |
| 
 | |
| 		// SBAS configuration for ublox 6 and higher
 | |
| 		p.Write(makeUBXCFG(0x06, 0x16, 8, []byte{0x01, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}))
 | |
| 
 | |
| 		// Message output configuration: UBX,00 (position) on each calculated fix; UBX,03 (satellite info) every 5th fix,
 | |
| 		//  UBX,04 (timing) every 10th, GGA (NMEA position) every 5th. All other NMEA messages disabled.
 | |
| 
 | |
| 		//                                             Msg   DDC   UART1 UART2 USB   I2C   Res
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x00, 0x00, 0x05, 0x00, 0x05, 0x00, 0x01})) // GGA enabled every 5th message
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})) // GLL disabled
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})) // GSA disabled
 | |
| 		//p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x02, 0x00, 0x05, 0x00, 0x05, 0x00, 0x01})) // GSA enabled disabled every 5th position (used for testing only)
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})) // GSV disabled
 | |
| 		//p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x03, 0x00, 0x05, 0x00, 0x05, 0x00, 0x01})) // GSV enabled for every 5th position (used for testing only)
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})) // RMC
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})) // VGT
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})) // GRS
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})) // GST
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})) // ZDA
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})) // GBS
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})) // DTM
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})) // GNS
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})) // ???
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})) // VLW
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF1, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00})) // Ublox,0
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF1, 0x03, 0x05, 0x05, 0x05, 0x05, 0x05, 0x00})) // Ublox,3
 | |
| 		p.Write(makeUBXCFG(0x06, 0x01, 8, []byte{0xF1, 0x04, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x00})) // Ublox,4
 | |
| 
 | |
| 		// Reconfigure serial port.
 | |
| 		cfg := make([]byte, 20)
 | |
| 		cfg[0] = 0x01 // portID.
 | |
| 		cfg[1] = 0x00 // res0.
 | |
| 		cfg[2] = 0x00 // res1.
 | |
| 		cfg[3] = 0x00 // res1.
 | |
| 
 | |
| 		//      [   7   ] [   6   ] [   5   ] [   4   ]
 | |
| 		//	0000 0000 0000 0000 0000 10x0 1100 0000
 | |
| 		// UART mode. 0 stop bits, no parity, 8 data bits. Little endian order.
 | |
| 		cfg[4] = 0xC0
 | |
| 		cfg[5] = 0x08
 | |
| 		cfg[6] = 0x00
 | |
| 		cfg[7] = 0x00
 | |
| 
 | |
| 		// Baud rate. Little endian order.
 | |
| 		bdrt := uint32(38400)
 | |
| 		cfg[11] = byte((bdrt >> 24) & 0xFF)
 | |
| 		cfg[10] = byte((bdrt >> 16) & 0xFF)
 | |
| 		cfg[9] = byte((bdrt >> 8) & 0xFF)
 | |
| 		cfg[8] = byte(bdrt & 0xFF)
 | |
| 
 | |
| 		// inProtoMask. NMEA and UBX. Little endian.
 | |
| 		cfg[12] = 0x03
 | |
| 		cfg[13] = 0x00
 | |
| 
 | |
| 		// outProtoMask. NMEA. Little endian.
 | |
| 		cfg[14] = 0x02
 | |
| 		cfg[15] = 0x00
 | |
| 
 | |
| 		cfg[16] = 0x00 // flags.
 | |
| 		cfg[17] = 0x00 // flags.
 | |
| 
 | |
| 		cfg[18] = 0x00 //pad.
 | |
| 		cfg[19] = 0x00 //pad.
 | |
| 
 | |
| 		p.Write(makeUBXCFG(0x06, 0x00, 20, cfg))
 | |
| 		//	time.Sleep(100* time.Millisecond) // pause and wait for the GPS to finish configuring itself before closing / reopening the port
 | |
| 		baudrate = 38400
 | |
| 
 | |
| 		if globalSettings.DEBUG {
 | |
| 			log.Printf("Finished writing u-blox GPS config to %s. Opening port to test connection.\n", device)
 | |
| 		}
 | |
| 	}
 | |
| 	p.Close()
 | |
| 
 | |
| 	time.Sleep(250 * time.Millisecond)
 | |
| 	// Re-open port at newly configured baud so we can read messages. ReadTimeout is set to keep from blocking the gpsSerialReader() on misconfigures or ttyAMA disconnects
 | |
| 	serialConfig = &serial.Config{Name: device, Baud: baudrate, ReadTimeout: time.Millisecond * 2500}
 | |
| 	p, err = serial.OpenPort(serialConfig)
 | |
| 	if err != nil {
 | |
| 		log.Printf("serial port err: %s\n", err.Error())
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	serialPort = p
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // func validateNMEAChecksum determines if a string is a properly formatted NMEA sentence with a valid checksum.
 | |
| //
 | |
| // If the input string is valid, output is the input stripped of the "$" token and checksum, along with a boolean 'true'
 | |
| // If the input string is the incorrect format, the checksum is missing/invalid, or checksum calculation fails, an error string and
 | |
| // boolean 'false' are returned
 | |
| //
 | |
| // Checksum is calculated as XOR of all bytes between "$" and "*"
 | |
| 
 | |
| func validateNMEAChecksum(s string) (string, bool) {
 | |
| 	//validate format. NMEA sentences start with "$" and end in "*xx" where xx is the XOR value of all bytes between
 | |
| 	if !(strings.HasPrefix(s, "$") && strings.Contains(s, "*")) {
 | |
| 		return "Invalid NMEA message", false
 | |
| 	}
 | |
| 
 | |
| 	// strip leading "$" and split at "*"
 | |
| 	s_split := strings.Split(strings.TrimPrefix(s, "$"), "*")
 | |
| 	s_out := s_split[0]
 | |
| 	s_cs := s_split[1]
 | |
| 
 | |
| 	if len(s_cs) < 2 {
 | |
| 		return "Missing checksum. Fewer than two bytes after asterisk", false
 | |
| 	}
 | |
| 
 | |
| 	cs, err := strconv.ParseUint(s_cs[:2], 16, 8)
 | |
| 	if err != nil {
 | |
| 		return "Invalid checksum", false
 | |
| 	}
 | |
| 
 | |
| 	cs_calc := byte(0)
 | |
| 	for i := range s_out {
 | |
| 		cs_calc = cs_calc ^ byte(s_out[i])
 | |
| 	}
 | |
| 
 | |
| 	if cs_calc != byte(cs) {
 | |
| 		return fmt.Sprintf("Checksum failed. Calculated %#X; expected %#X", cs_calc, cs), false
 | |
| 	}
 | |
| 
 | |
| 	return s_out, true
 | |
| }
 | |
| 
 | |
| //  Only count this heading if a "sustained" >7 kts is obtained. This filters out a lot of heading
 | |
| //  changes while on the ground and "movement" is really only changes in GPS fix as it settles down.
 | |
| //TODO: Some more robust checking above current and last speed.
 | |
| //TODO: Dynamic adjust for gain based on groundspeed
 | |
| func setTrueCourse(groundSpeed uint16, trueCourse float64) {
 | |
| 	if myMPU6050 != nil && globalStatus.RY835AI_connected && globalSettings.AHRS_Enabled {
 | |
| 		if mySituation.GroundSpeed >= 7 && groundSpeed >= 7 {
 | |
| 			myMPU6050.ResetHeading(trueCourse, 0.10)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func calculateNACp(accuracy float32) uint8 {
 | |
| 	ret := uint8(0)
 | |
| 
 | |
| 	if accuracy < 3 {
 | |
| 		ret = 11
 | |
| 	} else if accuracy < 10 {
 | |
| 		ret = 10
 | |
| 	} else if accuracy < 30 {
 | |
| 		ret = 9
 | |
| 	} else if accuracy < 92.6 {
 | |
| 		ret = 8
 | |
| 	} else if accuracy < 185.2 {
 | |
| 		ret = 7
 | |
| 	} else if accuracy < 555.6 {
 | |
| 		ret = 6
 | |
| 	}
 | |
| 
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| /*
 | |
| processNMEALine parses NMEA-0183 formatted strings against several message types.
 | |
| 
 | |
| Standard messages supported: RMC GGA VTG GSA
 | |
| U-blox proprietary messages: PUBX,00 PUBX,03 PUBX,04
 | |
| 
 | |
| return is false if errors occur during parse, or if GPS position is invalid
 | |
| return is true if parse occurs correctly and position is valid.
 | |
| 
 | |
| */
 | |
| 
 | |
| func processNMEALine(l string) (sentenceUsed bool) {
 | |
| 	mySituation.mu_GPS.Lock()
 | |
| 
 | |
| 	defer func() {
 | |
| 		if sentenceUsed || globalSettings.DEBUG {
 | |
| 			logSituation()
 | |
| 		}
 | |
| 		mySituation.mu_GPS.Unlock()
 | |
| 	}()
 | |
| 
 | |
| 	l_valid, validNMEAcs := validateNMEAChecksum(l)
 | |
| 	if !validNMEAcs {
 | |
| 		log.Printf("GPS error. Invalid NMEA string: %s\n", l_valid) // remove log message once validation complete
 | |
| 		return false
 | |
| 	}
 | |
| 	x := strings.Split(l_valid, ",")
 | |
| 
 | |
| 	mySituation.LastValidNMEAMessageTime = stratuxClock.Time
 | |
| 	mySituation.LastValidNMEAMessage = l
 | |
| 
 | |
| 	if x[0] == "PUBX" { // UBX proprietary message
 | |
| 		if x[1] == "00" { // Position fix.
 | |
| 			if len(x) < 20 {
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			tmpSituation := mySituation // If we decide to not use the data in this message, then don't make incomplete changes in mySituation.
 | |
| 
 | |
| 			// Do the accuracy / quality fields first to prevent invalid position etc. from being sent downstream
 | |
| 			// field 8 = nav status
 | |
| 			// DR = dead reckoning, G2= 2D GPS, G3 = 3D GPS, D2= 2D diff, D3 = 3D diff, RK = GPS+DR, TT = time only
 | |
| 			if x[8] == "D2" || x[8] == "D3" {
 | |
| 				tmpSituation.Quality = 2
 | |
| 			} else if x[8] == "G2" || x[8] == "G3" {
 | |
| 				tmpSituation.Quality = 1
 | |
| 			} else if x[8] == "DR" || x[8] == "RK" {
 | |
| 				tmpSituation.Quality = 6
 | |
| 			} else if x[8] == "NF" {
 | |
| 				tmpSituation.Quality = 0 // Just a note.
 | |
| 				return false
 | |
| 			} else {
 | |
| 				tmpSituation.Quality = 0 // Just a note.
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			// field 9 = horizontal accuracy, m
 | |
| 			hAcc, err := strconv.ParseFloat(x[9], 32)
 | |
| 			if err != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 			tmpSituation.Accuracy = float32(hAcc * 2) // UBX reports 1-sigma variation; NACp is 95% confidence (2-sigma)
 | |
| 
 | |
| 			// NACp estimate.
 | |
| 			tmpSituation.NACp = calculateNACp(tmpSituation.Accuracy)
 | |
| 
 | |
| 			// field 10 = vertical accuracy, m
 | |
| 			vAcc, err := strconv.ParseFloat(x[10], 32)
 | |
| 			if err != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 			tmpSituation.AccuracyVert = float32(vAcc * 2) // UBX reports 1-sigma variation; we want 95% confidence
 | |
| 
 | |
| 			// field 2 = time
 | |
| 			if len(x[2]) < 8 {
 | |
| 				return false
 | |
| 			}
 | |
| 			hr, err1 := strconv.Atoi(x[2][0:2])
 | |
| 			min, err2 := strconv.Atoi(x[2][2:4])
 | |
| 			sec, err3 := strconv.ParseFloat(x[2][4:], 32)
 | |
| 			if err1 != nil || err2 != nil || err3 != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			tmpSituation.LastFixSinceMidnightUTC = float32(3600*hr+60*min) + float32(sec)
 | |
| 
 | |
| 			// field 3-4 = lat
 | |
| 			if len(x[3]) < 10 {
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			hr, err1 = strconv.Atoi(x[3][0:2])
 | |
| 			minf, err2 := strconv.ParseFloat(x[3][2:], 32)
 | |
| 			if err1 != nil || err2 != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			tmpSituation.Lat = float32(hr) + float32(minf/60.0)
 | |
| 			if x[4] == "S" { // South = negative.
 | |
| 				tmpSituation.Lat = -tmpSituation.Lat
 | |
| 			}
 | |
| 
 | |
| 			// field 5-6 = lon
 | |
| 			if len(x[5]) < 11 {
 | |
| 				return false
 | |
| 			}
 | |
| 			hr, err1 = strconv.Atoi(x[5][0:3])
 | |
| 			minf, err2 = strconv.ParseFloat(x[5][3:], 32)
 | |
| 			if err1 != nil || err2 != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			tmpSituation.Lng = float32(hr) + float32(minf/60.0)
 | |
| 			if x[6] == "W" { // West = negative.
 | |
| 				tmpSituation.Lng = -tmpSituation.Lng
 | |
| 			}
 | |
| 
 | |
| 			// field 7 = height above ellipsoid, m
 | |
| 
 | |
| 			hae, err1 := strconv.ParseFloat(x[7], 32)
 | |
| 			if err1 != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 			alt := float32(hae*3.28084) - tmpSituation.GeoidSep        // convert to feet and offset by geoid separation
 | |
| 			tmpSituation.HeightAboveEllipsoid = float32(hae * 3.28084) // feet
 | |
| 			tmpSituation.Alt = alt
 | |
| 
 | |
| 			tmpSituation.LastFixLocalTime = stratuxClock.Time
 | |
| 
 | |
| 			// field 11 = groundspeed, km/h
 | |
| 			groundspeed, err := strconv.ParseFloat(x[11], 32)
 | |
| 			if err != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 			groundspeed = groundspeed * 0.540003 // convert to knots
 | |
| 			tmpSituation.GroundSpeed = uint16(groundspeed)
 | |
| 
 | |
| 			// field 12 = track, deg
 | |
| 			trueCourse := float32(0.0)
 | |
| 			tc, err := strconv.ParseFloat(x[12], 32)
 | |
| 			if err != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 			if groundspeed > 3 { // TO-DO: use average groundspeed over last n seconds to avoid random "jumps"
 | |
| 				trueCourse = float32(tc)
 | |
| 				setTrueCourse(uint16(groundspeed), tc)
 | |
| 				tmpSituation.TrueCourse = trueCourse
 | |
| 			} else {
 | |
| 				// Negligible movement. Don't update course, but do use the slow speed.
 | |
| 				// TO-DO: use average course over last n seconds?
 | |
| 			}
 | |
| 			tmpSituation.LastGroundTrackTime = stratuxClock.Time
 | |
| 
 | |
| 			// field 13 = vertical velocity, m/s
 | |
| 			vv, err := strconv.ParseFloat(x[13], 32)
 | |
| 			if err != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 			tmpSituation.GPSVertVel = float32(vv * -3.28084) // convert to ft/sec and positive = up
 | |
| 
 | |
| 			// field 14 = age of diff corrections
 | |
| 
 | |
| 			// field 18 = number of satellites
 | |
| 			sat, err1 := strconv.Atoi(x[18])
 | |
| 			if err1 != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 			tmpSituation.Satellites = uint16(sat) // this seems to be reliable. UBX,03 handles >12 satellites solutions correctly.
 | |
| 
 | |
| 			// We've made it this far, so that means we've processed "everything" and can now make the change to mySituation.
 | |
| 			mySituation = tmpSituation
 | |
| 			return true
 | |
| 		} else if x[1] == "03" { // satellite status message. Only the first 20 satellites will be reported in this message for UBX firmware older than v3.0. Order seems to be GPS, then SBAS, then GLONASS.
 | |
| 
 | |
| 			if len(x) < 3 { // malformed UBX,03 message that somehow passed checksum verification but is missing all of its fields
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			// field 2 = number of satellites tracked
 | |
| 			//satSeen := 0 // satellites seen (signal present)
 | |
| 			satTracked, err := strconv.Atoi(x[2])
 | |
| 			if err != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			if globalSettings.DEBUG {
 | |
| 				log.Printf("GPS PUBX,03 message with %d satellites is %d fields long. (Should be %d fields long)\n", satTracked, len(x), satTracked*6+3)
 | |
| 			}
 | |
| 
 | |
| 			if len(x) < (satTracked*6 + 3) { // malformed UBX,03 message that somehow passed checksum verification but is missing some of its fields
 | |
| 				if globalSettings.DEBUG {
 | |
| 					log.Printf("GPS PUBX,03 message is missing fields\n")
 | |
| 				}
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			mySituation.SatellitesTracked = uint16(satTracked) // requires UBX M8N firmware v3.01 or later to report > 20 satellites
 | |
| 
 | |
| 			// fields 3-8 are repeated block
 | |
| 
 | |
| 			var sv, elev, az, cno int
 | |
| 			var svType uint8
 | |
| 			var svStr string
 | |
| 
 | |
| 			/* Reference for constellation tracking
 | |
| 			for i:= 0; i < satTracked; i++ {
 | |
| 				x[3+6*i] // sv number
 | |
| 				x[4+6*i] // status [ U | e | - ] indicates [U]sed in solution, [e]phemeris data only, [-] not used
 | |
| 				x[5+6*i] // azimuth, deg, 0-359
 | |
| 				x[6+6*i] // elevation, deg, 0-90
 | |
| 				x[7+6*i] // signal strength dB-Hz
 | |
| 				x[8+6*i] // lock time, sec, 0-64
 | |
| 			}
 | |
| 			*/
 | |
| 
 | |
| 			for i := 0; i < satTracked; i++ {
 | |
| 				//field 3+6i is sv number. GPS NMEA = PRN. GLONASS NMEA = PRN + 65. SBAS is PRN; needs to be converted to NMEA for compatiblity with GSV messages.
 | |
| 				sv, err = strconv.Atoi(x[3+6*i]) // sv number
 | |
| 				if err != nil {
 | |
| 					return false
 | |
| 				}
 | |
| 				if sv < 33 { // indicates GPS
 | |
| 					svType = SAT_TYPE_GPS
 | |
| 					svStr = fmt.Sprintf("G%d", sv)
 | |
| 				} else if sv < 65 { // indicates SBAS: WAAS, EGNOS, MSAS, etc.
 | |
| 					svType = SAT_TYPE_SBAS
 | |
| 					svStr = fmt.Sprintf("S%d", sv+87) // add 87 to convert from NMEA to PRN.
 | |
| 				} else if sv < 97 { // GLONASS
 | |
| 					svType = SAT_TYPE_GLONASS
 | |
| 					svStr = fmt.Sprintf("R%d", sv-64) // subtract 64 to convert from NMEA to PRN.
 | |
| 				} else if (sv >= 120) && (sv < 162) { // indicates SBAS: WAAS, EGNOS, MSAS, etc.
 | |
| 					svType = SAT_TYPE_SBAS
 | |
| 					svStr = fmt.Sprintf("S%d", sv)
 | |
| 					sv -= 87 // subtract 87 to convert to NMEA from PRN.
 | |
| 				} else { // TO-DO: Galileo
 | |
| 					svType = SAT_TYPE_UNKNOWN
 | |
| 					svStr = fmt.Sprintf("U%d", sv)
 | |
| 				}
 | |
| 
 | |
| 				var thisSatellite SatelliteInfo
 | |
| 
 | |
| 				// START OF PROTECTED BLOCK
 | |
| 				satelliteMutex.Lock()
 | |
| 
 | |
| 				// Retrieve previous information on this satellite code.
 | |
| 				if val, ok := Satellites[svStr]; ok { // if we've already seen this satellite identifier, copy it in to do updates
 | |
| 					thisSatellite = val
 | |
| 					//log.Printf("UBX,03: Satellite %s already seen. Retrieving from 'Satellites'.\n", svStr) // DEBUG
 | |
| 				} else { // this satellite isn't in the Satellites data structure
 | |
| 					thisSatellite.SatelliteID = svStr
 | |
| 					thisSatellite.SatelliteNMEA = uint8(sv)
 | |
| 					thisSatellite.Type = uint8(svType)
 | |
| 					//log.Printf("UBX,03: Creating new satellite %s\n", svStr) // DEBUG
 | |
| 				}
 | |
| 				thisSatellite.TimeLastTracked = stratuxClock.Time
 | |
| 
 | |
| 				// Field 6+6*i is elevation, deg, 0-90
 | |
| 				elev, err = strconv.Atoi(x[6+6*i]) // elevation
 | |
| 				if err != nil {                    // could be blank if no position fix. Represent as -999.
 | |
| 					elev = -999
 | |
| 				}
 | |
| 				thisSatellite.Elevation = int16(elev)
 | |
| 
 | |
| 				// Field 5+6*i is azimuth, deg, 0-359
 | |
| 				az, err = strconv.Atoi(x[5+6*i]) // azimuth
 | |
| 				if err != nil {                  // could be blank if no position fix. Represent as -999.
 | |
| 					az = -999
 | |
| 				}
 | |
| 				thisSatellite.Azimuth = int16(az)
 | |
| 
 | |
| 				// Field 7+6*i is signal strength dB-Hz
 | |
| 				cno, err = strconv.Atoi(x[7+6*i]) // signal
 | |
| 				if err != nil {                   // will be blank if satellite isn't being received. Represent as -99.
 | |
| 					cno = -99
 | |
| 				} else if cno > 0 {
 | |
| 					thisSatellite.TimeLastSeen = stratuxClock.Time // Is this needed?
 | |
| 				}
 | |
| 				thisSatellite.Signal = int8(cno)
 | |
| 
 | |
| 				// Field 4+6*i is status: [ U | e | - ]: [U]sed in solution, [e]phemeris data only, [-] not used
 | |
| 				if x[4+6*i] == "U" {
 | |
| 					thisSatellite.InSolution = true
 | |
| 					thisSatellite.TimeLastSolution = stratuxClock.Time
 | |
| 				} else if x[4+6*i] == "e" {
 | |
| 					thisSatellite.InSolution = false
 | |
| 					//log.Printf("Satellite %s is no longer in solution but has ephemeris - UBX,03\n", svStr) // DEBUG
 | |
| 					// do anything that needs to be done for ephemeris
 | |
| 				} else {
 | |
| 					thisSatellite.InSolution = false
 | |
| 					//log.Printf("Satellite %s is no longer in solution and has no ephemeris - UBX,03\n", svStr) // DEBUG
 | |
| 				}
 | |
| 
 | |
| 				if globalSettings.DEBUG {
 | |
| 					inSolnStr := " "
 | |
| 					if thisSatellite.InSolution {
 | |
| 						inSolnStr = "+"
 | |
| 					}
 | |
| 					log.Printf("UBX: Satellite %s%s at index %d. Type = %d, NMEA-ID = %d, Elev = %d, Azimuth = %d, Cno = %d\n", inSolnStr, svStr, i, svType, sv, elev, az, cno) // remove later?
 | |
| 				}
 | |
| 
 | |
| 				Satellites[thisSatellite.SatelliteID] = thisSatellite // Update constellation with this satellite
 | |
| 				updateConstellation()
 | |
| 				satelliteMutex.Unlock()
 | |
| 				// END OF PROTECTED BLOCK
 | |
| 
 | |
| 				// end of satellite iteration loop
 | |
| 			}
 | |
| 
 | |
| 			return true
 | |
| 		} else if x[1] == "04" { // clock message
 | |
| 			// field 5 is UTC week (epoch = 1980-JAN-06). If this is invalid, do not parse date / time
 | |
| 			utcWeek, err0 := strconv.Atoi(x[5])
 | |
| 			if err0 != nil {
 | |
| 				// log.Printf("Error reading GPS week\n")
 | |
| 				return false
 | |
| 			}
 | |
| 			if utcWeek < 1877 || utcWeek >= 32767 { // unless we're in a flying Delorean, UTC dates before 2016-JAN-01 are not valid. Check underflow condition as well.
 | |
| 				if globalSettings.DEBUG {
 | |
| 					log.Printf("GPS week # %v out of scope; not setting time and date\n", utcWeek)
 | |
| 				}
 | |
| 				return false
 | |
| 			} /* else {
 | |
| 				log.Printf("GPS week # %v valid; evaluate time and date\n", utcWeek) //debug option
 | |
| 			} */
 | |
| 
 | |
| 			// field 2 is UTC time
 | |
| 			if len(x[2]) < 7 {
 | |
| 				return false
 | |
| 			}
 | |
| 			hr, err1 := strconv.Atoi(x[2][0:2])
 | |
| 			min, err2 := strconv.Atoi(x[2][2:4])
 | |
| 			sec, err3 := strconv.ParseFloat(x[2][4:], 32)
 | |
| 			if err1 != nil || err2 != nil || err3 != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 
 | |
| 			// field 3 is date
 | |
| 
 | |
| 			if len(x[3]) == 6 {
 | |
| 				// Date of Fix, i.e 191115 =  19 November 2015 UTC  field 9
 | |
| 				gpsTimeStr := fmt.Sprintf("%s %02d:%02d:%06.3f", x[3], hr, min, sec)
 | |
| 				gpsTime, err := time.Parse("020106 15:04:05.000", gpsTimeStr)
 | |
| 				if err == nil {
 | |
| 					// We only update ANY of the times if all of the time parsing is complete.
 | |
| 					mySituation.LastGPSTimeTime = stratuxClock.Time
 | |
| 					mySituation.GPSTime = gpsTime
 | |
| 					mySituation.LastFixSinceMidnightUTC = float32(3600*hr+60*min) + float32(sec)
 | |
| 					// log.Printf("GPS time is: %s\n", gpsTime) //debug
 | |
| 					if time.Since(gpsTime) > 3*time.Second || time.Since(gpsTime) < -3*time.Second {
 | |
| 						setStr := gpsTime.Format("20060102 15:04:05.000") + " UTC"
 | |
| 						log.Printf("setting system time to: '%s'\n", setStr)
 | |
| 						if err := exec.Command("date", "-s", setStr).Run(); err != nil {
 | |
| 							log.Printf("Set Date failure: %s error\n", err)
 | |
| 						} else {
 | |
| 							log.Printf("Time set from GPS. Current time is %v\n", time.Now())
 | |
| 						}
 | |
| 					}
 | |
| 					setDataLogTimeWithGPS(mySituation)
 | |
| 					return true // All possible successes lead here.
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// otherwise parse the NMEA standard messages as a compatibility option for SIRF, generic NMEA, etc.
 | |
| 	} else if (x[0] == "GNVTG") || (x[0] == "GPVTG") { // Ground track information.
 | |
| 		tmpSituation := mySituation // If we decide to not use the data in this message, then don't make incomplete changes in mySituation.
 | |
| 		if len(x) < 9 {             // Reduce from 10 to 9 to allow parsing by devices pre-NMEA v2.3
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		groundspeed, err := strconv.ParseFloat(x[5], 32) // Knots.
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.GroundSpeed = uint16(groundspeed)
 | |
| 
 | |
| 		trueCourse := float32(0)
 | |
| 		tc, err := strconv.ParseFloat(x[1], 32)
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		if groundspeed > 3 { // TO-DO: use average groundspeed over last n seconds to avoid random "jumps"
 | |
| 			trueCourse = float32(tc)
 | |
| 			setTrueCourse(uint16(groundspeed), tc)
 | |
| 			tmpSituation.TrueCourse = trueCourse
 | |
| 		} else {
 | |
| 			// Negligible movement. Don't update course, but do use the slow speed.
 | |
| 			// TO-DO: use average course over last n seconds?
 | |
| 		}
 | |
| 		tmpSituation.LastGroundTrackTime = stratuxClock.Time
 | |
| 
 | |
| 		// We've made it this far, so that means we've processed "everything" and can now make the change to mySituation.
 | |
| 		mySituation = tmpSituation
 | |
| 		return true
 | |
| 
 | |
| 	} else if (x[0] == "GNGGA") || (x[0] == "GPGGA") { // Position fix.
 | |
| 		tmpSituation := mySituation // If we decide to not use the data in this message, then don't make incomplete changes in mySituation.
 | |
| 
 | |
| 		if len(x) < 15 {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// Quality indicator.
 | |
| 		q, err1 := strconv.Atoi(x[6])
 | |
| 		if err1 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.Quality = uint8(q) // 1 = 3D GPS; 2 = DGPS (SBAS /WAAS)
 | |
| 
 | |
| 		// Timestamp.
 | |
| 		if len(x[1]) < 7 {
 | |
| 			return false
 | |
| 		}
 | |
| 		hr, err1 := strconv.Atoi(x[1][0:2])
 | |
| 		min, err2 := strconv.Atoi(x[1][2:4])
 | |
| 		sec, err3 := strconv.ParseFloat(x[1][4:], 32)
 | |
| 		if err1 != nil || err2 != nil || err3 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		tmpSituation.LastFixSinceMidnightUTC = float32(3600*hr+60*min) + float32(sec)
 | |
| 
 | |
| 		// Latitude.
 | |
| 		if len(x[2]) < 4 {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		hr, err1 = strconv.Atoi(x[2][0:2])
 | |
| 		minf, err2 := strconv.ParseFloat(x[2][2:], 32)
 | |
| 		if err1 != nil || err2 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		tmpSituation.Lat = float32(hr) + float32(minf/60.0)
 | |
| 		if x[3] == "S" { // South = negative.
 | |
| 			tmpSituation.Lat = -tmpSituation.Lat
 | |
| 		}
 | |
| 
 | |
| 		// Longitude.
 | |
| 		if len(x[4]) < 5 {
 | |
| 			return false
 | |
| 		}
 | |
| 		hr, err1 = strconv.Atoi(x[4][0:3])
 | |
| 		minf, err2 = strconv.ParseFloat(x[4][3:], 32)
 | |
| 		if err1 != nil || err2 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		tmpSituation.Lng = float32(hr) + float32(minf/60.0)
 | |
| 		if x[5] == "W" { // West = negative.
 | |
| 			tmpSituation.Lng = -tmpSituation.Lng
 | |
| 		}
 | |
| 
 | |
| 		// Altitude.
 | |
| 		alt, err1 := strconv.ParseFloat(x[9], 32)
 | |
| 		if err1 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.Alt = float32(alt * 3.28084) // Convert to feet.
 | |
| 
 | |
| 		// Geoid separation (Sep = HAE - MSL)
 | |
| 		// (needed for proper MSL offset on PUBX,00 altitudes)
 | |
| 
 | |
| 		geoidSep, err1 := strconv.ParseFloat(x[11], 32)
 | |
| 		if err1 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.GeoidSep = float32(geoidSep * 3.28084) // Convert to feet.
 | |
| 		tmpSituation.HeightAboveEllipsoid = tmpSituation.GeoidSep + tmpSituation.Alt
 | |
| 
 | |
| 		// Timestamp.
 | |
| 		tmpSituation.LastFixLocalTime = stratuxClock.Time
 | |
| 
 | |
| 		// We've made it this far, so that means we've processed "everything" and can now make the change to mySituation.
 | |
| 		mySituation = tmpSituation
 | |
| 		return true
 | |
| 
 | |
| 	} else if (x[0] == "GNRMC") || (x[0] == "GPRMC") { // Recommended Minimum data. FIXME: Is this needed anymore?
 | |
| 		tmpSituation := mySituation // If we decide to not use the data in this message, then don't make incomplete changes in mySituation.
 | |
| 
 | |
| 		//$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
 | |
| 		/*						check RY835 man for NMEA version, if >2.2, add mode field
 | |
| 				Where:
 | |
| 		     RMC          Recommended Minimum sentence C
 | |
| 		     123519       Fix taken at 12:35:19 UTC
 | |
| 		     A            Status A=active or V=Void.
 | |
| 		     4807.038,N   Latitude 48 deg 07.038' N
 | |
| 		     01131.000,E  Longitude 11 deg 31.000' E
 | |
| 		     022.4        Speed over the ground in knots
 | |
| 		     084.4        Track angle in degrees True
 | |
| 		     230394       Date - 23rd of March 1994
 | |
| 		     003.1,W      Magnetic Variation
 | |
| 		     D				mode field (nmea 2.3 and higher)
 | |
| 		     *6A          The checksum data, always begins with *
 | |
| 		*/
 | |
| 		if len(x) < 11 {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		if x[2] != "A" { // invalid fix
 | |
| 			tmpSituation.Quality = 0 // Just a note.
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// Timestamp.
 | |
| 		if len(x[1]) < 7 {
 | |
| 			return false
 | |
| 		}
 | |
| 		hr, err1 := strconv.Atoi(x[1][0:2])
 | |
| 		min, err2 := strconv.Atoi(x[1][2:4])
 | |
| 		sec, err3 := strconv.ParseFloat(x[1][4:], 32)
 | |
| 		if err1 != nil || err2 != nil || err3 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.LastFixSinceMidnightUTC = float32(3600*hr+60*min) + float32(sec)
 | |
| 
 | |
| 		if len(x[9]) == 6 {
 | |
| 			// Date of Fix, i.e 191115 =  19 November 2015 UTC  field 9
 | |
| 			gpsTimeStr := fmt.Sprintf("%s %02d:%02d:%06.3f", x[9], hr, min, sec)
 | |
| 			gpsTime, err := time.Parse("020106 15:04:05.000", gpsTimeStr)
 | |
| 			if err == nil {
 | |
| 				tmpSituation.LastGPSTimeTime = stratuxClock.Time
 | |
| 				tmpSituation.GPSTime = gpsTime
 | |
| 				if time.Since(gpsTime) > 3*time.Second || time.Since(gpsTime) < -3*time.Second {
 | |
| 					setStr := gpsTime.Format("20060102 15:04:05.000") + " UTC"
 | |
| 					log.Printf("setting system time to: '%s'\n", setStr)
 | |
| 					if err := exec.Command("date", "-s", setStr).Run(); err != nil {
 | |
| 						log.Printf("Set Date failure: %s error\n", err)
 | |
| 					} else {
 | |
| 						log.Printf("Time set from GPS. Current time is %v\n", time.Now())
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Latitude.
 | |
| 		if len(x[3]) < 4 {
 | |
| 			return false
 | |
| 		}
 | |
| 		hr, err1 = strconv.Atoi(x[3][0:2])
 | |
| 		minf, err2 := strconv.ParseFloat(x[3][2:], 32)
 | |
| 		if err1 != nil || err2 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.Lat = float32(hr) + float32(minf/60.0)
 | |
| 		if x[4] == "S" { // South = negative.
 | |
| 			tmpSituation.Lat = -tmpSituation.Lat
 | |
| 		}
 | |
| 		// Longitude.
 | |
| 		if len(x[5]) < 5 {
 | |
| 			return false
 | |
| 		}
 | |
| 		hr, err1 = strconv.Atoi(x[5][0:3])
 | |
| 		minf, err2 = strconv.ParseFloat(x[5][3:], 32)
 | |
| 		if err1 != nil || err2 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.Lng = float32(hr) + float32(minf/60.0)
 | |
| 		if x[6] == "W" { // West = negative.
 | |
| 			tmpSituation.Lng = -tmpSituation.Lng
 | |
| 		}
 | |
| 
 | |
| 		tmpSituation.LastFixLocalTime = stratuxClock.Time
 | |
| 
 | |
| 		// ground speed in kts (field 7)
 | |
| 		groundspeed, err := strconv.ParseFloat(x[7], 32)
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.GroundSpeed = uint16(groundspeed)
 | |
| 
 | |
| 		// ground track "True" (field 8)
 | |
| 		trueCourse := float32(0)
 | |
| 		tc, err := strconv.ParseFloat(x[8], 32)
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		if groundspeed > 3 { // TO-DO: use average groundspeed over last n seconds to avoid random "jumps"
 | |
| 			trueCourse = float32(tc)
 | |
| 			setTrueCourse(uint16(groundspeed), tc)
 | |
| 			tmpSituation.TrueCourse = trueCourse
 | |
| 		} else {
 | |
| 			// Negligible movement. Don't update course, but do use the slow speed.
 | |
| 			// TO-DO: use average course over last n seconds?
 | |
| 		}
 | |
| 
 | |
| 		tmpSituation.LastGroundTrackTime = stratuxClock.Time
 | |
| 
 | |
| 		// We've made it this far, so that means we've processed "everything" and can now make the change to mySituation.
 | |
| 		mySituation = tmpSituation
 | |
| 		setDataLogTimeWithGPS(mySituation)
 | |
| 		return true
 | |
| 
 | |
| 	} else if (x[0] == "GNGSA") || (x[0] == "GPGSA") { // Satellite data.
 | |
| 		tmpSituation := mySituation // If we decide to not use the data in this message, then don't make incomplete changes in mySituation.
 | |
| 
 | |
| 		if len(x) < 18 {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// field 1: operation mode
 | |
| 		// M: manual forced to 2D or 3D mode
 | |
| 		// A: automatic switching between 2D and 3D modes
 | |
| 
 | |
| 		/*
 | |
| 			if (x[1] != "A") && (x[1] != "M") { // invalid fix ... but x[2] is a better indicator of fix quality. Deprecating this.
 | |
| 				tmpSituation.Quality = 0 // Just a note.
 | |
| 				return false
 | |
| 			}
 | |
| 		*/
 | |
| 
 | |
| 		// field 2: solution type
 | |
| 		// 1 = no solution; 2 = 2D fix, 3 = 3D fix. WAAS status is parsed from GGA message, so no need to get here
 | |
| 		if (x[2] == "") || (x[2] == "1") { // missing or no solution
 | |
| 			tmpSituation.Quality = 0 // Just a note.
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// fields 3-14: satellites in solution
 | |
| 		var svStr string
 | |
| 		var svType uint8
 | |
| 		var svSBAS bool    // used to indicate whether this GSA message contains a SBAS satellite
 | |
| 		var svGLONASS bool // used to indicate whether this GSA message contains GLONASS satellites
 | |
| 		sat := 0
 | |
| 
 | |
| 		for _, svtxt := range x[3:15] {
 | |
| 			sv, err := strconv.Atoi(svtxt)
 | |
| 			if err == nil {
 | |
| 				sat++
 | |
| 
 | |
| 				if sv < 33 { // indicates GPS
 | |
| 					svType = SAT_TYPE_GPS
 | |
| 					svStr = fmt.Sprintf("G%d", sv)
 | |
| 				} else if sv < 65 { // indicates SBAS: WAAS, EGNOS, MSAS, etc.
 | |
| 					svType = SAT_TYPE_SBAS
 | |
| 					svStr = fmt.Sprintf("S%d", sv+87) // add 87 to convert from NMEA to PRN.
 | |
| 					svSBAS = true
 | |
| 				} else if sv < 97 { // GLONASS
 | |
| 					svType = SAT_TYPE_GLONASS
 | |
| 					svStr = fmt.Sprintf("R%d", sv-64) // subtract 64 to convert from NMEA to PRN.
 | |
| 					svGLONASS = true
 | |
| 				} else { // TO-DO: Galileo
 | |
| 					svType = SAT_TYPE_UNKNOWN
 | |
| 					svStr = fmt.Sprintf("U%d", sv)
 | |
| 				}
 | |
| 
 | |
| 				var thisSatellite SatelliteInfo
 | |
| 
 | |
| 				// START OF PROTECTED BLOCK
 | |
| 				satelliteMutex.Lock()
 | |
| 
 | |
| 				// Retrieve previous information on this satellite code.
 | |
| 				if val, ok := Satellites[svStr]; ok { // if we've already seen this satellite identifier, copy it in to do updates
 | |
| 					thisSatellite = val
 | |
| 					//log.Printf("Satellite %s already seen. Retrieving from 'Satellites'.\n", svStr)
 | |
| 				} else { // this satellite isn't in the Satellites data structure, so create it
 | |
| 					thisSatellite.SatelliteID = svStr
 | |
| 					thisSatellite.SatelliteNMEA = uint8(sv)
 | |
| 					thisSatellite.Type = uint8(svType)
 | |
| 					//log.Printf("Creating new satellite %s from GSA message\n", svStr) // DEBUG
 | |
| 				}
 | |
| 				thisSatellite.InSolution = true
 | |
| 				thisSatellite.TimeLastSolution = stratuxClock.Time
 | |
| 				thisSatellite.TimeLastSeen = stratuxClock.Time    // implied, since this satellite is used in the position solution
 | |
| 				thisSatellite.TimeLastTracked = stratuxClock.Time // implied, since this satellite is used in the position solution
 | |
| 
 | |
| 				Satellites[thisSatellite.SatelliteID] = thisSatellite // Update constellation with this satellite
 | |
| 				updateConstellation()
 | |
| 				satelliteMutex.Unlock()
 | |
| 				// END OF PROTECTED BLOCK
 | |
| 
 | |
| 			}
 | |
| 		}
 | |
| 		if sat < 12 || tmpSituation.Satellites < 13 { // GSA only reports up to 12 satellites in solution, so we don't want to overwrite higher counts based on updateConstellation().
 | |
| 			tmpSituation.Satellites = uint16(sat)
 | |
| 			if (tmpSituation.Quality == 2) && !svSBAS && !svGLONASS { // add one to the satellite count if we have a SBAS solution, but the GSA message doesn't track a SBAS satellite
 | |
| 				tmpSituation.Satellites++
 | |
| 			}
 | |
| 		}
 | |
| 		//log.Printf("There are %d satellites in solution from this GSA message\n", sat) // TESTING - DEBUG
 | |
| 
 | |
| 		// field 16: HDOP
 | |
| 		// Accuracy estimate
 | |
| 		hdop, err1 := strconv.ParseFloat(x[16], 32)
 | |
| 		if err1 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		if tmpSituation.Quality == 2 {
 | |
| 			tmpSituation.Accuracy = float32(hdop * 4.0) // Rough 95% confidence estimate for WAAS / DGPS solution
 | |
| 		} else {
 | |
| 			tmpSituation.Accuracy = float32(hdop * 8.0) // Rough 95% confidence estimate for 3D non-WAAS solution
 | |
| 		}
 | |
| 
 | |
| 		// NACp estimate.
 | |
| 		tmpSituation.NACp = calculateNACp(tmpSituation.Accuracy)
 | |
| 
 | |
| 		// field 17: VDOP
 | |
| 		// accuracy estimate
 | |
| 		vdop, err1 := strconv.ParseFloat(x[17], 32)
 | |
| 		if err1 != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		tmpSituation.AccuracyVert = float32(vdop * 5) // rough estimate for 95% confidence
 | |
| 
 | |
| 		// We've made it this far, so that means we've processed "everything" and can now make the change to mySituation.
 | |
| 		mySituation = tmpSituation
 | |
| 		return true
 | |
| 
 | |
| 	}
 | |
| 
 | |
| 	if (x[0] == "GPGSV") || (x[0] == "GLGSV") { // GPS + SBAS or GLONASS satellites in view message. Galileo is TBD.
 | |
| 		if len(x) < 4 {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// field 1 = number of GSV messages of this type
 | |
| 		msgNum, err := strconv.Atoi(x[2])
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// field 2 = index of this GSV message
 | |
| 
 | |
| 		msgIndex, err := strconv.Atoi(x[2])
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// field 3 = number of GPS satellites tracked
 | |
| 		/* Is this redundant if parsing from full constellation?
 | |
| 		satTracked, err := strconv.Atoi(x[3])
 | |
| 		if err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		*/
 | |
| 
 | |
| 		//mySituation.SatellitesTracked = uint16(satTracked) // Replaced with parsing of 'Satellites' data structure
 | |
| 
 | |
| 		// field 4-7 = repeating block with satellite id, elevation, azimuth, and signal strengh (Cno)
 | |
| 
 | |
| 		lenGSV := len(x)
 | |
| 		satsThisMsg := (lenGSV - 4) / 4
 | |
| 
 | |
| 		if globalSettings.DEBUG {
 | |
| 			log.Printf("%s message [%d of %d] is %v fields long and describes %v satellites\n", x[0], msgIndex, msgNum, lenGSV, satsThisMsg)
 | |
| 		}
 | |
| 
 | |
| 		var sv, elev, az, cno int
 | |
| 		var svType uint8
 | |
| 		var svStr string
 | |
| 
 | |
| 		for i := 0; i < satsThisMsg; i++ {
 | |
| 
 | |
| 			sv, err = strconv.Atoi(x[4+4*i]) // sv number
 | |
| 			if err != nil {
 | |
| 				return false
 | |
| 			}
 | |
| 			if sv < 33 { // indicates GPS
 | |
| 				svType = SAT_TYPE_GPS
 | |
| 				svStr = fmt.Sprintf("G%d", sv)
 | |
| 			} else if sv < 65 { // indicates SBAS: WAAS, EGNOS, MSAS, etc.
 | |
| 				svType = SAT_TYPE_SBAS
 | |
| 				svStr = fmt.Sprintf("S%d", sv+87) // add 87 to convert from NMEA to PRN.
 | |
| 			} else if sv < 97 { // GLONASS
 | |
| 				svType = SAT_TYPE_GLONASS
 | |
| 				svStr = fmt.Sprintf("R%d", sv-64) // subtract 64 to convert from NMEA to PRN.
 | |
| 			} else { // TO-DO: Galileo
 | |
| 				svType = SAT_TYPE_UNKNOWN
 | |
| 				svStr = fmt.Sprintf("U%d", sv)
 | |
| 			}
 | |
| 
 | |
| 			var thisSatellite SatelliteInfo
 | |
| 
 | |
| 			// START OF PROTECTED BLOCK
 | |
| 			satelliteMutex.Lock()
 | |
| 
 | |
| 			// Retrieve previous information on this satellite code.
 | |
| 			if val, ok := Satellites[svStr]; ok { // if we've already seen this satellite identifier, copy it in to do updates
 | |
| 				thisSatellite = val
 | |
| 				//log.Printf("Satellite %s already seen. Retrieving from 'Satellites'.\n", svStr) // DEBUG
 | |
| 			} else { // this satellite isn't in the Satellites data structure, so create it new
 | |
| 				thisSatellite.SatelliteID = svStr
 | |
| 				thisSatellite.SatelliteNMEA = uint8(sv)
 | |
| 				thisSatellite.Type = uint8(svType)
 | |
| 				//log.Printf("Creating new satellite %s\n", svStr) // DEBUG
 | |
| 			}
 | |
| 			thisSatellite.TimeLastTracked = stratuxClock.Time
 | |
| 
 | |
| 			elev, err = strconv.Atoi(x[5+4*i]) // elevation
 | |
| 			if err != nil {                    // some firmwares leave this blank if there's no position fix. Represent as -999.
 | |
| 				elev = -999
 | |
| 			}
 | |
| 			thisSatellite.Elevation = int16(elev)
 | |
| 
 | |
| 			az, err = strconv.Atoi(x[6+4*i]) // azimuth
 | |
| 			if err != nil {                  // UBX allows tracking up to 5(?) degrees below horizon. Some firmwares leave this blank if no position fix. Represent invalid as -999.
 | |
| 				az = -999
 | |
| 			}
 | |
| 			thisSatellite.Azimuth = int16(az)
 | |
| 
 | |
| 			cno, err = strconv.Atoi(x[7+4*i]) // signal
 | |
| 			if err != nil {                   // will be blank if satellite isn't being received. Represent as -99.
 | |
| 				cno = -99
 | |
| 				thisSatellite.InSolution = false // resets the "InSolution" status if the satellite disappears out of solution due to no signal. FIXME
 | |
| 				//log.Printf("Satellite %s is no longer in solution due to cno parse error - GSV\n", svStr) // DEBUG
 | |
| 			} else if cno > 0 {
 | |
| 				thisSatellite.TimeLastSeen = stratuxClock.Time // Is this needed?
 | |
| 			}
 | |
| 			if cno > 127 { // make sure strong signals don't overflow. Normal range is 0-99 so it shouldn't, but take no chances.
 | |
| 				cno = 127
 | |
| 			}
 | |
| 			thisSatellite.Signal = int8(cno)
 | |
| 
 | |
| 			// hack workaround for GSA 12-sv limitation... if this is a SBAS satellite, we have a SBAS solution, and signal is greater than some arbitrary threshold, set InSolution
 | |
| 			// drawback is this will show all tracked SBAS satellites as being in solution.
 | |
| 			if thisSatellite.Type == SAT_TYPE_SBAS {
 | |
| 				if mySituation.Quality == 2 {
 | |
| 					if thisSatellite.Signal > 16 {
 | |
| 						thisSatellite.InSolution = true
 | |
| 						thisSatellite.TimeLastSolution = stratuxClock.Time
 | |
| 					}
 | |
| 				} else { // quality == 0 or 1
 | |
| 					thisSatellite.InSolution = false
 | |
| 					//log.Printf("WAAS satellite %s is marked as out of solution GSV\n", svStr) // DEBUG
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if globalSettings.DEBUG {
 | |
| 				inSolnStr := " "
 | |
| 				if thisSatellite.InSolution {
 | |
| 					inSolnStr = "+"
 | |
| 				}
 | |
| 				log.Printf("GSV: Satellite %s%s at index %d. Type = %d, NMEA-ID = %d, Elev = %d, Azimuth = %d, Cno = %d\n", inSolnStr, svStr, i, svType, sv, elev, az, cno) // remove later?
 | |
| 			}
 | |
| 
 | |
| 			Satellites[thisSatellite.SatelliteID] = thisSatellite // Update constellation with this satellite
 | |
| 			updateConstellation()
 | |
| 			satelliteMutex.Unlock()
 | |
| 			// END OF PROTECTED BLOCK
 | |
| 		}
 | |
| 
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	// if we've gotten this far, the message isn't one that we want to parse
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func gpsSerialReader() {
 | |
| 	defer serialPort.Close()
 | |
| 	readyToInitGPS = false // TO-DO: replace with channel control to terminate goroutine when complete
 | |
| 
 | |
| 	i := 0 //debug monitor
 | |
| 	scanner := bufio.NewScanner(serialPort)
 | |
| 	for scanner.Scan() && globalStatus.GPS_connected && globalSettings.GPS_Enabled {
 | |
| 		i++
 | |
| 		if globalSettings.DEBUG && i%100 == 0 {
 | |
| 			log.Printf("gpsSerialReader() scanner loop iteration i=%d\n", i) // debug monitor
 | |
| 		}
 | |
| 
 | |
| 		s := scanner.Text()
 | |
| 
 | |
| 		if !processNMEALine(s) {
 | |
| 			if globalSettings.DEBUG {
 | |
| 				fmt.Printf("processNMEALine() exited early -- %s\n", s)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if err := scanner.Err(); err != nil {
 | |
| 		log.Printf("reading standard input: %s\n", err.Error())
 | |
| 	}
 | |
| 
 | |
| 	if globalSettings.DEBUG {
 | |
| 		log.Printf("Exiting gpsSerialReader() after i=%d loops\n", i) // debug monitor
 | |
| 	}
 | |
| 	globalStatus.GPS_connected = false
 | |
| 	readyToInitGPS = true // TO-DO: replace with channel control to terminate goroutine when complete
 | |
| 	return
 | |
| }
 | |
| 
 | |
| var i2cbus embd.I2CBus
 | |
| var myBMP180 *bmp180.BMP180
 | |
| var myMPU6050 *mpu6050.MPU6050
 | |
| 
 | |
| func readBMP180() (float64, float64, error) { // ºCelsius, Meters
 | |
| 	temp, err := myBMP180.Temperature()
 | |
| 	if err != nil {
 | |
| 		return temp, 0.0, err
 | |
| 	}
 | |
| 	altitude, err := myBMP180.Altitude()
 | |
| 	altitude = float64(1/0.3048) * altitude // Convert meters to feet.
 | |
| 	if err != nil {
 | |
| 		return temp, altitude, err
 | |
| 	}
 | |
| 	return temp, altitude, nil
 | |
| }
 | |
| 
 | |
| func readMPU6050() (float64, float64, error) { //TODO: error checking.
 | |
| 	pitch, roll := myMPU6050.PitchAndRoll()
 | |
| 	return pitch, roll, nil
 | |
| }
 | |
| 
 | |
| func initBMP180() error {
 | |
| 	myBMP180 = bmp180.New(i2cbus) //TODO: error checking.
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func initMPU6050() error {
 | |
| 	myMPU6050 = mpu6050.New() //TODO: error checking.
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func initI2C() error {
 | |
| 	i2cbus = embd.NewI2CBus(1) //TODO: error checking.
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Unused at the moment. 5 second update, since read functions in bmp180 are slow.
 | |
| func tempAndPressureReader() {
 | |
| 	timer := time.NewTicker(5 * time.Second)
 | |
| 	for globalStatus.RY835AI_connected && globalSettings.AHRS_Enabled {
 | |
| 		<-timer.C
 | |
| 		// Read temperature and pressure altitude.
 | |
| 		temp, alt, err_bmp180 := readBMP180()
 | |
| 		// Process.
 | |
| 		if err_bmp180 != nil {
 | |
| 			log.Printf("readBMP180(): %s\n", err_bmp180.Error())
 | |
| 			globalStatus.RY835AI_connected = false
 | |
| 		} else {
 | |
| 			mySituation.Temp = temp
 | |
| 			mySituation.Pressure_alt = alt
 | |
| 			mySituation.LastTempPressTime = stratuxClock.Time
 | |
| 		}
 | |
| 	}
 | |
| 	globalStatus.RY835AI_connected = false
 | |
| }
 | |
| 
 | |
| func makeFFAHRSSimReport() {
 | |
| 	s := fmt.Sprintf("XATTStratux,%f,%f,%f", mySituation.Gyro_heading, mySituation.Pitch, mySituation.Roll)
 | |
| 
 | |
| 	sendMsg([]byte(s), NETWORK_AHRS_FFSIM, false)
 | |
| }
 | |
| 
 | |
| func makeAHRSGDL90Report() {
 | |
| 	msg := make([]byte, 16)
 | |
| 	msg[0] = 0x4c
 | |
| 	msg[1] = 0x45
 | |
| 	msg[2] = 0x01
 | |
| 	msg[3] = 0x00
 | |
| 
 | |
| 	pitch := int16(float64(mySituation.Pitch) * float64(10.0))
 | |
| 	roll := int16(float64(mySituation.Roll) * float64(10.0))
 | |
| 	hdg := uint16(float64(mySituation.Gyro_heading) * float64(10.0))
 | |
| 	slip_skid := int16(float64(0) * float64(10.0))
 | |
| 	yaw_rate := int16(float64(0) * float64(10.0))
 | |
| 	g := int16(float64(1.0) * float64(10.0))
 | |
| 
 | |
| 	// Roll.
 | |
| 	msg[4] = byte((roll >> 8) & 0xFF)
 | |
| 	msg[5] = byte(roll & 0xFF)
 | |
| 
 | |
| 	// Pitch.
 | |
| 	msg[6] = byte((pitch >> 8) & 0xFF)
 | |
| 	msg[7] = byte(pitch & 0xFF)
 | |
| 
 | |
| 	// Heading.
 | |
| 	msg[8] = byte((hdg >> 8) & 0xFF)
 | |
| 	msg[9] = byte(hdg & 0xFF)
 | |
| 
 | |
| 	// Slip/skid.
 | |
| 	msg[10] = byte((slip_skid >> 8) & 0xFF)
 | |
| 	msg[11] = byte(slip_skid & 0xFF)
 | |
| 
 | |
| 	// Yaw rate.
 | |
| 	msg[12] = byte((yaw_rate >> 8) & 0xFF)
 | |
| 	msg[13] = byte(yaw_rate & 0xFF)
 | |
| 
 | |
| 	// "G".
 | |
| 	msg[14] = byte((g >> 8) & 0xFF)
 | |
| 	msg[15] = byte(g & 0xFF)
 | |
| 
 | |
| 	sendMsg(prepareMessage(msg), NETWORK_AHRS_GDL90, false)
 | |
| }
 | |
| 
 | |
| func attitudeReaderSender() {
 | |
| 	timer := time.NewTicker(100 * time.Millisecond) // ~10Hz update.
 | |
| 	for globalStatus.RY835AI_connected && globalSettings.AHRS_Enabled {
 | |
| 		<-timer.C
 | |
| 		// Read pitch and roll.
 | |
| 		pitch, roll, err_mpu6050 := readMPU6050()
 | |
| 
 | |
| 		if err_mpu6050 != nil {
 | |
| 			log.Printf("readMPU6050(): %s\n", err_mpu6050.Error())
 | |
| 			globalStatus.RY835AI_connected = false
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		mySituation.mu_Attitude.Lock()
 | |
| 
 | |
| 		mySituation.Pitch = pitch
 | |
| 		mySituation.Roll = roll
 | |
| 		mySituation.Gyro_heading = myMPU6050.Heading() //FIXME. Experimental.
 | |
| 		mySituation.LastAttitudeTime = stratuxClock.Time
 | |
| 
 | |
| 		// Send, if valid.
 | |
| 		//		if isGPSGroundTrackValid(), etc.
 | |
| 
 | |
| 		// makeFFAHRSSimReport() // simultaneous use of GDL90 and FFSIM not supported in FF 7.5.1 or later. Function definition will be kept for AHRS debugging and future workarounds.
 | |
| 		makeAHRSGDL90Report()
 | |
| 
 | |
| 		mySituation.mu_Attitude.Unlock()
 | |
| 	}
 | |
| 	globalStatus.RY835AI_connected = false
 | |
| }
 | |
| 
 | |
| /*
 | |
| 	updateConstellation(): Periodic cleanup and statistics calculation for 'Satellites'
 | |
| 		data structure. Calling functions must protect this in a satelliteMutex.
 | |
| 
 | |
| */
 | |
| 
 | |
| func updateConstellation() {
 | |
| 	var sats, tracked, seen uint8
 | |
| 	for svStr, thisSatellite := range Satellites {
 | |
| 		if stratuxClock.Since(thisSatellite.TimeLastTracked) > 10*time.Second { // remove stale satellites if they haven't been tracked for 10 seconds
 | |
| 			delete(Satellites, svStr)
 | |
| 		} else { // satellite almanac data is "fresh" even if it isn't being received.
 | |
| 			tracked++
 | |
| 			if thisSatellite.Signal > 0 {
 | |
| 				seen++
 | |
| 			}
 | |
| 			if stratuxClock.Since(thisSatellite.TimeLastSolution) > 5*time.Second {
 | |
| 				thisSatellite.InSolution = false
 | |
| 				Satellites[svStr] = thisSatellite
 | |
| 			}
 | |
| 			if thisSatellite.InSolution { // TESTING: Determine "In solution" from structure (fix for multi-GNSS overflow)
 | |
| 				sats++
 | |
| 			}
 | |
| 			// do any other calculations needed for this satellite
 | |
| 		}
 | |
| 	}
 | |
| 	//log.Printf("Satellite counts: %d tracking channels, %d with >0 dB-Hz signal\n", tracked, seen) // DEBUG - REMOVE
 | |
| 	//log.Printf("Satellite struct: %v\n", Satellites)                                               // DEBUG - REMOVE
 | |
| 	mySituation.Satellites = uint16(sats)
 | |
| 	mySituation.SatellitesTracked = uint16(tracked)
 | |
| 	mySituation.SatellitesSeen = uint16(seen)
 | |
| }
 | |
| 
 | |
| func isGPSConnected() bool {
 | |
| 	return stratuxClock.Since(mySituation.LastValidNMEAMessageTime) < 5*time.Second
 | |
| }
 | |
| 
 | |
| /*
 | |
| isGPSValid returns true only if a valid position fix has been seen in the last 15 seconds,
 | |
| and if the GPS subsystem has recently detected a GPS device.
 | |
| 
 | |
| If false, 'Quality` is set to 0 ("No fix"), as is the number of satellites in solution.
 | |
| */
 | |
| 
 | |
| func isGPSValid() bool {
 | |
| 	isValid := false
 | |
| 	if (stratuxClock.Since(mySituation.LastFixLocalTime) < 15*time.Second) && globalStatus.GPS_connected && mySituation.Quality > 0 {
 | |
| 		isValid = true
 | |
| 	} else {
 | |
| 		mySituation.Quality = 0
 | |
| 		mySituation.Satellites = 0
 | |
| 	}
 | |
| 	return isValid
 | |
| }
 | |
| 
 | |
| func isGPSGroundTrackValid() bool {
 | |
| 	return stratuxClock.Since(mySituation.LastGroundTrackTime) < 15*time.Second
 | |
| }
 | |
| 
 | |
| func isGPSClockValid() bool {
 | |
| 	return stratuxClock.Since(mySituation.LastGPSTimeTime) < 15*time.Second
 | |
| }
 | |
| 
 | |
| func isAHRSValid() bool {
 | |
| 	return stratuxClock.Since(mySituation.LastAttitudeTime) < 1*time.Second // If attitude information gets to be over 1 second old, declare invalid.
 | |
| }
 | |
| 
 | |
| func isTempPressValid() bool {
 | |
| 	return stratuxClock.Since(mySituation.LastTempPressTime) < 15*time.Second
 | |
| }
 | |
| 
 | |
| func initAHRS() error {
 | |
| 	if err := initI2C(); err != nil { // I2C bus.
 | |
| 		return err
 | |
| 	}
 | |
| 	if err := initBMP180(); err != nil { // I2C temperature and pressure altitude.
 | |
| 		i2cbus.Close()
 | |
| 		return err
 | |
| 	}
 | |
| 	if err := initMPU6050(); err != nil { // I2C accel/gyro.
 | |
| 		i2cbus.Close()
 | |
| 		myBMP180.Close()
 | |
| 		return err
 | |
| 	}
 | |
| 	globalStatus.RY835AI_connected = true
 | |
| 	go attitudeReaderSender()
 | |
| 	go tempAndPressureReader()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func pollRY835AI() {
 | |
| 	readyToInitGPS = true //TO-DO: Implement more robust method (channel control) to kill zombie serial readers
 | |
| 	timer := time.NewTicker(4 * time.Second)
 | |
| 	for {
 | |
| 		<-timer.C
 | |
| 		// GPS enabled, was not connected previously?
 | |
| 		if globalSettings.GPS_Enabled && !globalStatus.GPS_connected && readyToInitGPS { //TO-DO: Implement more robust method (channel control) to kill zombie serial readers
 | |
| 			globalStatus.GPS_connected = initGPSSerial()
 | |
| 			if globalStatus.GPS_connected {
 | |
| 				go gpsSerialReader()
 | |
| 			}
 | |
| 		}
 | |
| 		// RY835AI I2C enabled, was not connected previously?
 | |
| 		if globalSettings.AHRS_Enabled && !globalStatus.RY835AI_connected {
 | |
| 			err := initAHRS()
 | |
| 			if err != nil {
 | |
| 				log.Printf("initAHRS(): %s\ndisabling AHRS sensors.\n", err.Error())
 | |
| 				globalStatus.RY835AI_connected = false
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func initRY835AI() {
 | |
| 	mySituation.mu_GPS = &sync.Mutex{}
 | |
| 	mySituation.mu_Attitude = &sync.Mutex{}
 | |
| 	satelliteMutex = &sync.Mutex{}
 | |
| 	Satellites = make(map[string]SatelliteInfo)
 | |
| 
 | |
| 	go pollRY835AI()
 | |
| }
 |