protocol/aprs: refactored
This commit is contained in:
@@ -2,312 +2,498 @@ package aprs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.maze.io/go/ham/util/maidenhead"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// Position ambiguity replacement
|
||||
disambiguation = []int{2, 3, 5, 6, 12, 13, 15, 16}
|
||||
|
||||
miceCodes = map[rune]map[int]string{
|
||||
'0': {0: "0", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'1': {0: "1", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'2': {0: "2", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'3': {0: "3", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'4': {0: "4", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'5': {0: "5", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'6': {0: "6", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'7': {0: "7", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'8': {0: "8", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'9': {0: "9", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'A': {0: "0", 1: "1 (Custom)"},
|
||||
'B': {0: "1", 1: "1 (Custom)"},
|
||||
'C': {0: "2", 1: "1 (Custom)"},
|
||||
'D': {0: "3", 1: "1 (Custom)"},
|
||||
'E': {0: "4", 1: "1 (Custom)"},
|
||||
'F': {0: "5", 1: "1 (Custom)"},
|
||||
'G': {0: "6", 1: "1 (Custom)"},
|
||||
'H': {0: "7", 1: "1 (Custom)"},
|
||||
'I': {0: "8", 1: "1 (Custom)"},
|
||||
'J': {0: "9", 1: "1 (Custom)"},
|
||||
'K': {0: " ", 1: "1 (Custom)"},
|
||||
'L': {0: " ", 1: "0", 2: "S", 3: "0", 4: "E"},
|
||||
'P': {0: "0", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'Q': {0: "1", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'R': {0: "2", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'S': {0: "3", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'T': {0: "4", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'U': {0: "5", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'V': {0: "6", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'W': {0: "7", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'X': {0: "8", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'Y': {0: "9", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
'Z': {0: " ", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
||||
}
|
||||
|
||||
miceMsgTypes = map[string]string{
|
||||
"000": "Emergency",
|
||||
"001 (Std)": "Priority",
|
||||
"001 (Custom)": "Custom-6",
|
||||
"010 (Std)": "Special",
|
||||
"010 (Custom)": "Custom-5",
|
||||
"011 (Std)": "Committed",
|
||||
"011 (Custom)": "Custom-4",
|
||||
"100 (Std)": "Returning",
|
||||
"100 (Custom)": "Custom-3",
|
||||
"101 (Std)": "In Service",
|
||||
"101 (Custom)": "Custom-2",
|
||||
"110 (Std)": "En Route",
|
||||
"110 (Custom)": "Custom-1",
|
||||
"111 (Std)": "Off Duty",
|
||||
"111 (Custom)": "Custom-0",
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
gridChars = "ABCDEFGHIJKLMNOPQRSTUVWX0123456789"
|
||||
|
||||
messageTypeStd = "Std"
|
||||
messageTypeCustom = "Custom"
|
||||
)
|
||||
|
||||
// Position contains GPS coordinates.
|
||||
type Position struct {
|
||||
Latitude float64 `json:"latitude"` // Degrees
|
||||
Longitude float64 `json:"longitude"` // Degrees
|
||||
Ambiguity int `json:"ambiguity,omitempty"`
|
||||
Symbol Symbol `json:"symbol"`
|
||||
Compressed bool `json:"compressed,omitempty"`
|
||||
HasMessaging bool `json:"has_messaging"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
Altitude float64 `json:"altitude,omitempty"` // Altitude (in meters)
|
||||
Range float64 `json:"range,omitempty"` // Radio range (in meters)
|
||||
IsCompressed bool `json:"is_compressed"`
|
||||
CompressedGPSFix uint8 `json:"-"`
|
||||
CompressedNMEASource uint8 `json:"-"`
|
||||
CompressedOrigin uint8 `json:"-"`
|
||||
Time time.Time `json:"time"`
|
||||
Comment string `json:"comment"`
|
||||
Symbol string `json:"symbol"`
|
||||
Velocity *Velocity `json:"velocity,omitempty"` // Velocity encoded in the payload.
|
||||
Wind *Wind `json:"wind,omitempty"` // Wind direction and speed.
|
||||
PHG *PowerHeightGain `json:"phg,omitempty"`
|
||||
DFS *OmniDFStrength `json:"dfs,omitempty"`
|
||||
Weather *Weather `json:"weather,omitempty"`
|
||||
Telemetry *Telemetry `json:"telemetry,omitempty"` // Telemetry data
|
||||
}
|
||||
|
||||
func (pos Position) String() string {
|
||||
if pos.Ambiguity == 0 {
|
||||
return fmt.Sprintf("{%f, %f}", pos.Latitude, pos.Longitude)
|
||||
}
|
||||
return fmt.Sprintf("{%f, %f}, ambiguity=%d", pos.Latitude, pos.Longitude, pos.Ambiguity)
|
||||
func (pos *Position) String() string {
|
||||
return "position"
|
||||
}
|
||||
|
||||
func ParseUncompressedPosition(s string) (Position, string, error) {
|
||||
// APRS PROTOCOL REFERENCE 1.0.1 Chapter 8, page 32 (42 in PDF)
|
||||
// Velocity details.
|
||||
type Velocity struct {
|
||||
Course int // degrees
|
||||
Speed float64 // meters per second
|
||||
}
|
||||
|
||||
pos := Position{}
|
||||
// Wind details.
|
||||
type Wind struct {
|
||||
Direction float64 // degrees
|
||||
Speed float64 // meters per second
|
||||
}
|
||||
|
||||
if len(s) < 18 {
|
||||
return pos, "", ErrInvalidPosition
|
||||
// PowerHeightGain details.
|
||||
type PowerHeightGain struct {
|
||||
PowerCode byte
|
||||
HeightCode byte
|
||||
GainCode byte
|
||||
DirectivityCode byte
|
||||
}
|
||||
|
||||
// OmniDFStrength contains the omni-directional direction finding signal strength (for fox hunting).
|
||||
type OmniDFStrength struct {
|
||||
StrengthCode byte
|
||||
HeightCode byte
|
||||
GainCode byte
|
||||
DirectivityCode byte
|
||||
}
|
||||
|
||||
type positionDecoder struct{}
|
||||
|
||||
func (d positionDecoder) CanDecode(frame *Frame) bool {
|
||||
switch frame.Raw.Type() {
|
||||
case '!', '=', '/', '@': // compressed/uncompressed with/without messaging
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
b := []byte(s)
|
||||
for _, p := range disambiguation {
|
||||
if b[p] == ' ' {
|
||||
pos.Ambiguity++
|
||||
b[p] = '0'
|
||||
}
|
||||
}
|
||||
s = string(b)
|
||||
|
||||
func (d positionDecoder) Decode(frame *Frame) (data Data, err error) {
|
||||
var (
|
||||
err error
|
||||
latDeg, latMin, latMinFrag uint64
|
||||
lngDeg, lngMin, lngMinFrag uint64
|
||||
latHemi, lngHemi byte
|
||||
isSouth, isWest bool
|
||||
raw = frame.Raw
|
||||
kind = raw.Type()
|
||||
hasTimestamp = kind == '@' || kind == '/'
|
||||
hasMessaging = kind == '@' || kind == '='
|
||||
content = string(raw[1:])
|
||||
pos = &Position{
|
||||
HasMessaging: hasMessaging,
|
||||
}
|
||||
)
|
||||
|
||||
if latDeg, err = strconv.ParseUint(s[0:2], 10, 8); err != nil {
|
||||
return pos, "", err
|
||||
}
|
||||
if latMin, err = strconv.ParseUint(s[2:4], 10, 8); err != nil {
|
||||
return pos, "", err
|
||||
}
|
||||
if latMinFrag, err = strconv.ParseUint(s[5:7], 10, 8); err != nil {
|
||||
return pos, "", err
|
||||
}
|
||||
latHemi = s[7]
|
||||
pos.Symbol[0] = s[8]
|
||||
if lngDeg, err = strconv.ParseUint(s[9:12], 10, 8); err != nil {
|
||||
return pos, "", err
|
||||
}
|
||||
if lngMin, err = strconv.ParseUint(s[12:14], 10, 8); err != nil {
|
||||
return pos, "", err
|
||||
}
|
||||
if lngMinFrag, err = strconv.ParseUint(s[15:17], 10, 8); err != nil {
|
||||
return pos, "", err
|
||||
}
|
||||
lngHemi = s[17]
|
||||
pos.Symbol[1] = s[18]
|
||||
|
||||
if latHemi == 'S' || latHemi == 's' {
|
||||
isSouth = true
|
||||
} else if latHemi != 'N' && latHemi != 'n' {
|
||||
return pos, "", ErrInvalidPosition
|
||||
}
|
||||
|
||||
if lngHemi == 'W' || lngHemi == 'w' {
|
||||
isWest = true
|
||||
} else if lngHemi != 'E' && lngHemi != 'e' {
|
||||
return pos, "", ErrInvalidPosition
|
||||
}
|
||||
|
||||
if latDeg > 89 || lngDeg > 179 {
|
||||
return pos, "", ErrInvalidPosition
|
||||
}
|
||||
|
||||
pos.Latitude = float64(latDeg) + float64(latMin)/60.0 + float64(latMinFrag)/6000.0
|
||||
pos.Longitude = float64(lngDeg) + float64(lngMin)/60.0 + float64(lngMinFrag)/6000.0
|
||||
|
||||
if isSouth {
|
||||
pos.Latitude = 0.0 - pos.Latitude
|
||||
}
|
||||
if isWest {
|
||||
pos.Longitude = 0.0 - pos.Longitude
|
||||
}
|
||||
|
||||
if pos.Symbol[1] >= 'a' || pos.Symbol[1] <= 'k' {
|
||||
pos.Symbol[1] -= 32
|
||||
}
|
||||
|
||||
if len(s) > 19 {
|
||||
return pos, s[19:], nil
|
||||
}
|
||||
return pos, "", nil
|
||||
}
|
||||
|
||||
func ParseCompressedPosition(s string) (Position, string, error) {
|
||||
// APRS PROTOCOL REFERENCE 1.0.1 Chapter 9, page 36 (46 in PDF)
|
||||
|
||||
pos := Position{}
|
||||
|
||||
if len(s) < 10 {
|
||||
return pos, "", ErrInvalidPosition
|
||||
}
|
||||
|
||||
// Base-91 check
|
||||
for _, c := range s[1:9] {
|
||||
if c < 0x21 || c > 0x7b {
|
||||
return pos, "", ErrInvalidPosition
|
||||
if hasTimestamp {
|
||||
if len(content) < 7 {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
if pos.Time, _, err = parseTimestamp(content[:7]); err != nil {
|
||||
return
|
||||
}
|
||||
content = content[7:]
|
||||
}
|
||||
|
||||
var err error
|
||||
var lat, lng int
|
||||
if lat, err = base91Decode(s[1:5]); err != nil {
|
||||
return pos, "", err
|
||||
if err = pos.parsePositionAndComment(content); err != nil {
|
||||
return
|
||||
}
|
||||
if lng, err = base91Decode(s[5:9]); err != nil {
|
||||
return pos, "", err
|
||||
}
|
||||
|
||||
pos.Latitude = 90.0 - float64(lat)/380926.0
|
||||
pos.Longitude = -180.0 + float64(lng)/190463.0
|
||||
pos.Compressed = true
|
||||
|
||||
return pos, s[10:], nil
|
||||
}
|
||||
|
||||
func ParseMicE(s, dest string) (Position, error) {
|
||||
// APRS PROTOCOL REFERENCE 1.0.1 Chapter 10, page 42 in PDF
|
||||
|
||||
pos := Position{}
|
||||
|
||||
if len(s) < 9 || len(dest) != 6 {
|
||||
return pos, ErrInvalidPosition
|
||||
}
|
||||
|
||||
ns := miceCodes[rune(dest[3])][2]
|
||||
we := miceCodes[rune(dest[5])][4]
|
||||
|
||||
latF := fmt.Sprintf("%s%s", miceCodes[rune(dest[0])][0], miceCodes[rune(dest[1])][0])
|
||||
latF = strings.Trim(latF, ". ")
|
||||
latD, err := strconv.ParseFloat(latF, 64)
|
||||
if err != nil {
|
||||
return pos, ErrInvalidPosition
|
||||
}
|
||||
lonF := fmt.Sprintf("%s%s.%s%s", miceCodes[rune(dest[2])][0], miceCodes[rune(dest[3])][0], miceCodes[rune(dest[4])][0], miceCodes[rune(dest[5])][0])
|
||||
lonF = strings.Trim(lonF, ". ")
|
||||
latM, err := strconv.ParseFloat(lonF, 64)
|
||||
if err != nil {
|
||||
return pos, ErrInvalidPosition
|
||||
}
|
||||
if latM != 0 {
|
||||
latD += latM / 60
|
||||
}
|
||||
if strings.ToUpper(ns) == "S" {
|
||||
latD = -latD
|
||||
}
|
||||
|
||||
lonOff := miceCodes[rune(dest[4])][3]
|
||||
lonD := float64(s[1]) - 28
|
||||
if lonOff == "100" {
|
||||
lonD += 100
|
||||
}
|
||||
if lonD >= 180 && lonD < 190 {
|
||||
lonD -= 80
|
||||
}
|
||||
if lonD >= 190 && lonD < 200 {
|
||||
lonD -= 190
|
||||
}
|
||||
|
||||
lonM := float64(s[2]) - 28
|
||||
if lonM >= 60 {
|
||||
lonM -= 60
|
||||
}
|
||||
// adding hundreth of minute then add minute as deg fraction
|
||||
lonH := float64(s[3]) - 28
|
||||
if lonH != 0 {
|
||||
lonM += lonH / 100
|
||||
}
|
||||
if lonM != 0 {
|
||||
lonD += lonM / 60
|
||||
}
|
||||
if strings.ToUpper(we) == "W" {
|
||||
lonD = -lonD
|
||||
}
|
||||
|
||||
pos.Latitude = latD
|
||||
pos.Longitude = lonD
|
||||
|
||||
return pos, nil
|
||||
}
|
||||
|
||||
func ParsePositionGrid(s string) (Position, string, error) {
|
||||
var o int
|
||||
for o = 0; o < len(s); o++ {
|
||||
if strings.IndexByte(gridChars, s[o]) < 0 {
|
||||
func (pos *Position) parsePositionAndComment(s string) (err error) {
|
||||
var comment string
|
||||
//log.Printf("parse position and comment %q: %t", s, isDigit(s[0]))
|
||||
if isDigit(s[0]) {
|
||||
// probably not compressed
|
||||
if comment, err = pos.parseNotCompressedPosition(s); err != nil {
|
||||
return
|
||||
}
|
||||
pos.IsCompressed = false
|
||||
} else {
|
||||
// probably compressed
|
||||
if comment, err = pos.parseCompressedPosition(s); err != nil {
|
||||
return
|
||||
}
|
||||
pos.IsCompressed = true
|
||||
}
|
||||
|
||||
if len(comment) > 0 {
|
||||
if err = pos.parseAltitudeWeatherAndExtension(comment); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (pos *Position) parseCompressedPosition(raw string) (comment string, err error) {
|
||||
if len(raw) < 10 {
|
||||
return "", fmt.Errorf("aprs: invalid compressed position string of length %d", len(raw))
|
||||
}
|
||||
|
||||
pos.Symbol = string([]byte{raw[0], raw[9]})
|
||||
|
||||
var lat, lng int
|
||||
if lat, err = base91Decode(raw[1:5]); err != nil {
|
||||
return
|
||||
}
|
||||
if lng, err = base91Decode(raw[5:9]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pos.Latitude = 90.0 - float64(lat)/380926.0
|
||||
pos.Longitude = -180.0 + float64(lng)/190463.0
|
||||
pos.IsCompressed = true
|
||||
|
||||
comment = raw[13:]
|
||||
if len(comment) >= 3 {
|
||||
// Compression Type (T) Byte Format
|
||||
//
|
||||
// Bit: 7 | 6 | 5 | 4 3 | 2 1 0 |
|
||||
// -------+--------+---------+-------------+------------------+
|
||||
// Unused | Unused | GPS Fix | NMEA Source | Origin |
|
||||
// -------+--------+---------+-------------+------------------+
|
||||
// Val: 0 | 0 | 0 = old | 00 = other | 000 = Compressed |
|
||||
// | | 1 = cur | 01 = GLL | 001 = TNC BTex |
|
||||
// | | | 10 = CGA | 010 = Software |
|
||||
// | | | 11 = RMC | 011 = [tbd] |
|
||||
// | | | | 100 = KPC3 |
|
||||
// | | | | 101 = Pico |
|
||||
// | | | | 110 = Other |
|
||||
// | | | | 111 = Digipeater |
|
||||
var (
|
||||
c = comment[0] - 33
|
||||
s = comment[1] - 33
|
||||
T = comment[2] - 33
|
||||
)
|
||||
if raw[10] == ' ' {
|
||||
// Don't do any further processing
|
||||
} else if ((T >> 3) & 3) == 2 {
|
||||
// CGA sentence, NMEA Source = 0b10
|
||||
var altitudeFeet int
|
||||
if altitudeFeet, err = base91Decode(comment[:2]); err != nil {
|
||||
return
|
||||
}
|
||||
pos.Altitude = feetToMeters(float64(altitudeFeet))
|
||||
pos.CompressedGPSFix = (T >> 5) & 0x01
|
||||
pos.CompressedNMEASource = (T >> 3) & 0x03
|
||||
pos.CompressedOrigin = T & 0x07
|
||||
comment = comment[3:]
|
||||
} else if comment[0] >= '!' && comment[0] <= 'z' { // !..z
|
||||
// Course/speed
|
||||
pos.Velocity = &Velocity{
|
||||
Course: int(c) * 4,
|
||||
Speed: knotsToMetersPerSecond(math.Pow(1.08, float64(s))),
|
||||
}
|
||||
pos.CompressedGPSFix = (T >> 5) & 0x01
|
||||
pos.CompressedNMEASource = (T >> 3) & 0x03
|
||||
pos.CompressedOrigin = T & 0x07
|
||||
comment = comment[3:]
|
||||
} else if comment[0] == '{' { // {
|
||||
// Precalculated range
|
||||
pos.Range = milesToMeters(2 * math.Pow(1.08, float64(s)))
|
||||
comment = comment[3:]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (pos *Position) parseNotCompressedPosition(s string) (comment string, err error) {
|
||||
//log.Printf("parse not compressed: %q", s)
|
||||
if len(s) < 19 {
|
||||
return "", fmt.Errorf("aprs: invalid not compressed position string of length %d", len(s))
|
||||
}
|
||||
|
||||
pos.Symbol = string([]byte{s[8], s[18]})
|
||||
if pos.Latitude, err = parseDMLatitude(s[:8]); err != nil {
|
||||
return
|
||||
}
|
||||
if pos.Longitude, err = parseDMLongitude(s[9:18]); err != nil {
|
||||
return
|
||||
}
|
||||
return s[19:], nil
|
||||
}
|
||||
|
||||
var (
|
||||
matchVelocity = regexp.MustCompile(`^[. 0-3][. 0-9]{2,2}/[. 0-9]{3}`)
|
||||
matchPHG = regexp.MustCompile(`^PHG[0-9]{3,3}[0-8]`)
|
||||
matchDFS = regexp.MustCompile(`^DFS[0-9]{3,3}[0-8]`)
|
||||
matchRNG = regexp.MustCompile(`^RNG[0-9]{4}`)
|
||||
matchAreaObject = regexp.MustCompile(``)
|
||||
)
|
||||
|
||||
func (pos *Position) parseAltitudeWeatherAndExtension(s string) (err error) {
|
||||
//log.Printf("parse altitude, weather and extensions %q", s)
|
||||
var comment string
|
||||
|
||||
// Parse extensions
|
||||
switch {
|
||||
case matchVelocity.MatchString(s):
|
||||
var course, speed int
|
||||
if course, err = strconv.Atoi(fillZeros(s[:3])); err != nil {
|
||||
return fmt.Errorf("invalid course: %v", err)
|
||||
}
|
||||
if speed, err = strconv.Atoi(fillZeros(s[4:7])); err != nil {
|
||||
return fmt.Errorf("invalid speed: %v", err)
|
||||
}
|
||||
pos.Velocity = &Velocity{
|
||||
Course: int(course),
|
||||
Speed: knotsToMetersPerSecond(float64(speed)),
|
||||
}
|
||||
comment = s[7:]
|
||||
if len(comment) > 2 && comment[0] == '/' && isDigit(comment[1]) && isDigit(comment[2]) {
|
||||
var dir int
|
||||
if dir, err = strconv.Atoi(fillZeros(comment[1:4])); err != nil {
|
||||
return fmt.Errorf("invalid wind direction: %v", err)
|
||||
}
|
||||
if speed, err = strconv.Atoi(fillZeros(comment[5:8])); err != nil {
|
||||
return fmt.Errorf("invalid wind speed: %v", err)
|
||||
}
|
||||
pos.Wind = &Wind{
|
||||
Direction: float64(dir),
|
||||
Speed: knotsToMetersPerSecond(float64(speed)),
|
||||
}
|
||||
comment = comment[8:]
|
||||
}
|
||||
|
||||
case matchPHG.MatchString(s):
|
||||
comment = s[7:]
|
||||
|
||||
case matchDFS.MatchString(s):
|
||||
comment = s[7:]
|
||||
|
||||
case matchRNG.MatchString(s):
|
||||
comment = s[7:]
|
||||
|
||||
default:
|
||||
comment = s
|
||||
}
|
||||
|
||||
//log.Printf("after extensions: %q", comment)
|
||||
|
||||
if pos.Altitude, comment, err = parseAltitude(comment); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//log.Printf("after altitude, before weather: %q", comment)
|
||||
|
||||
if pos.Weather, comment, err = parseWeather(comment); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//log.Printf("after weather, before telemetry: %q", comment)
|
||||
|
||||
if pos.Telemetry, comment, err = parseBase91Telemetry(comment); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pos.Comment = comment
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseAltitude(s string) (altitude float64, comment string, err error) {
|
||||
const altitudeMarker = "/A="
|
||||
if i := strings.Index(s, altitudeMarker); i >= 0 {
|
||||
//log.Printf("parse altitude marker: %q", s[i+3:])
|
||||
var feet int
|
||||
if feet, err = strconv.Atoi(strings.TrimSpace(s[i+3 : i+3+6])); err != nil {
|
||||
return 0, "", fmt.Errorf("aprs: invalid altitude: %v", err)
|
||||
}
|
||||
return feetToMeters(float64(feet)), s[:i] + s[i+3+6:], nil
|
||||
}
|
||||
return 0, s, nil
|
||||
}
|
||||
|
||||
var weatherSymbolSize = map[byte]int{
|
||||
'g': 3, // peak wind speed in the past 5 minutes, in mph
|
||||
't': 3, // temperature in degrees Fahrenheit
|
||||
'r': 3, // rainfall in hundredths of an inch
|
||||
'p': 3, // rainfall in hundredths of an inch
|
||||
'P': 3, // rainfall in hundredths of an inch
|
||||
'h': 2, // relative humidity in %
|
||||
'b': 5, // barometric pressure in tenths of millibars
|
||||
'l': 3, // luminosity (in Watts per square meter) 1000 and above
|
||||
'L': 3, // luminosity (in Watts per square meter) 999 and below
|
||||
's': 3, // snowfall in the last 24 hours in inches
|
||||
'#': 3, // raw rain counter
|
||||
}
|
||||
|
||||
// Weather report.
|
||||
type Weather struct {
|
||||
WindGust float64 `json:"windGust"` // wind gust in m/s
|
||||
Temperature float64 `json:"temperature"` // temperature (in degrees C)
|
||||
Rain1h float64 `json:"rain1h"` // rain in the last hour (in mm)
|
||||
Rain24h float64 `json:"rain24h"` // rain in the last day (in mm)
|
||||
RainSinceMidnight float64 `json:"rainSinceMidnight"` // rain since midnight (in mm)
|
||||
RainRaw int `json:"rainRaw"` // rain raw counter
|
||||
Humidity float64 `json:"humidity"` // relative humidity (in %)
|
||||
Pressure float64 `json:"pressure"` // pressure (in mBar)
|
||||
Luminosity float64 `json:"luminosity"` // luminosity (in W/m^2)
|
||||
Snowfall float64 `json:"snowfall"` // snowfall (in cm/day)
|
||||
}
|
||||
|
||||
func parseWeather(s string) (weather *Weather, comment string, err error) {
|
||||
comment = s
|
||||
for len(comment) > 0 {
|
||||
if size, ok := weatherSymbolSize[comment[0]]; ok {
|
||||
if len(comment[1:]) < size {
|
||||
return nil, "", fmt.Errorf("aprs: not enough characters to encode weather symbol %c (%d < %d)", comment[0], len(comment[1:]), size)
|
||||
}
|
||||
|
||||
var value float64
|
||||
if value, err = strconv.ParseFloat(comment[1:size+1], 64); err != nil {
|
||||
// Something else that started with a weather symbol, perhaps a comment, stop parsing
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
|
||||
if weather == nil {
|
||||
weather = new(Weather)
|
||||
}
|
||||
|
||||
switch comment[0] {
|
||||
case 'g':
|
||||
weather.WindGust = value * 0.4470
|
||||
case 't':
|
||||
weather.Temperature = fahrenheitToCelcius(value)
|
||||
case 'r':
|
||||
weather.Rain1h = value * 0.254
|
||||
case 'p':
|
||||
weather.Rain24h = value * 0.254
|
||||
case 'P':
|
||||
weather.RainSinceMidnight = value * 0.254
|
||||
case 'h':
|
||||
weather.Humidity = value
|
||||
case 'b':
|
||||
weather.Pressure = value / 10
|
||||
case 'l':
|
||||
weather.Luminosity = value + 1000
|
||||
case 'L':
|
||||
weather.Luminosity = value
|
||||
case 's':
|
||||
weather.Snowfall = value * 2.54
|
||||
case '#':
|
||||
weather.RainRaw = int(value)
|
||||
}
|
||||
|
||||
comment = comment[1+size:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pos := Position{}
|
||||
if o == 2 || o == 4 || o == 6 || o == 8 {
|
||||
p, err := maidenhead.ParseLocator(s[:o])
|
||||
if err != nil {
|
||||
return pos, "", err
|
||||
return
|
||||
}
|
||||
|
||||
type Telemetry struct {
|
||||
ID int
|
||||
Analog []int
|
||||
Digital []bool
|
||||
}
|
||||
|
||||
var matchTelemetry = regexp.MustCompile(`\|([!-{]{4,14})\|`)
|
||||
|
||||
func parseBase91Telemetry(s string) (telemetry *Telemetry, comment string, err error) {
|
||||
var i int
|
||||
if i = strings.IndexByte(s, '|'); i == -1 {
|
||||
return nil, s, nil
|
||||
}
|
||||
|
||||
var sequence string
|
||||
comment, sequence = s[:i], s[i+1:]
|
||||
if i = strings.IndexByte(sequence, '|'); i < 1 {
|
||||
// no closing | found, return as comment
|
||||
return nil, s, nil
|
||||
}
|
||||
|
||||
if sequence, comment = sequence[:i], comment+sequence[i+1:]; len(sequence)%2 != 0 {
|
||||
// uneven number of sequence elements,
|
||||
return nil, s, nil
|
||||
}
|
||||
|
||||
telemetry = new(Telemetry)
|
||||
if telemetry.ID, err = base91Decode(sequence[:2]); err != nil {
|
||||
// it wasn't base-91 encoded telemetry, return data as comment
|
||||
return nil, s, nil
|
||||
}
|
||||
|
||||
var values []int
|
||||
for i = 2; i < len(sequence); i += 2 {
|
||||
var value int
|
||||
if value, err = base91Decode(sequence[i : i+2]); err != nil {
|
||||
// it wasn't base-91 encoded telemetry, return data as comment
|
||||
return nil, s, nil
|
||||
}
|
||||
pos.Latitude = p.Latitude
|
||||
pos.Longitude = p.Longitude
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
var txt string
|
||||
if o < len(s) {
|
||||
txt = s[o+1:]
|
||||
if len(values) > 5 {
|
||||
for i = 0; i < 8; i++ {
|
||||
telemetry.Digital = append(telemetry.Digital, (values[5]&1) == 1)
|
||||
values[5] >>= 1
|
||||
}
|
||||
values = values[:5]
|
||||
}
|
||||
return pos, txt, nil
|
||||
telemetry.Analog = values
|
||||
return
|
||||
}
|
||||
|
||||
func ParsePosition(s string, compressed bool) (Position, string, error) {
|
||||
if compressed {
|
||||
return ParseCompressedPosition(s)
|
||||
func parseDMLatitude(s string) (v float64, err error) {
|
||||
if len(s) != 8 || s[4] != '.' || !(s[7] == 'N' || s[7] == 'S') {
|
||||
return 0, fmt.Errorf("aprs: invalid latitude %q", s)
|
||||
}
|
||||
return ParseUncompressedPosition(s)
|
||||
|
||||
s = strings.Replace(s, " ", "0", -1) // position ambiguity
|
||||
|
||||
var (
|
||||
degs, mins, minFrags int
|
||||
south = s[7] == 'S'
|
||||
)
|
||||
if degs, err = strconv.Atoi(s[:2]); err != nil {
|
||||
return
|
||||
}
|
||||
if mins, err = strconv.Atoi(s[2:4]); err != nil {
|
||||
return
|
||||
}
|
||||
if minFrags, err = strconv.Atoi(s[5:7]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v = float64(degs) + float64(mins)/60 + float64(minFrags)/6000
|
||||
if south {
|
||||
return -v, nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func ParsePositionBoth(s string) (Position, string, error) {
|
||||
pos, txt, err := ParseUncompressedPosition(s)
|
||||
if err != nil {
|
||||
return ParseCompressedPosition(s)
|
||||
func parseDMLongitude(s string) (v float64, err error) {
|
||||
if len(s) != 9 || s[5] != '.' || !(s[8] == 'W' || s[8] == 'E') {
|
||||
return 0, fmt.Errorf("aprs: invalid longitude %q", s)
|
||||
}
|
||||
return pos, txt, err
|
||||
|
||||
s = strings.Replace(s, " ", "0", -1) // position ambiguity
|
||||
|
||||
var (
|
||||
degs, mins, minFrags int
|
||||
east = s[8] == 'E'
|
||||
)
|
||||
if degs, err = strconv.Atoi(s[:3]); err != nil {
|
||||
return
|
||||
}
|
||||
if mins, err = strconv.Atoi(s[3:5]); err != nil {
|
||||
return
|
||||
}
|
||||
if minFrags, err = strconv.Atoi(s[6:8]); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v = float64(degs) + float64(mins)/60 + float64(minFrags)/6000
|
||||
if east {
|
||||
return v, nil
|
||||
}
|
||||
return -v, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user