Initial import
This commit is contained in:
481
protocol/aprs/packet.go
Normal file
481
protocol/aprs/packet.go
Normal file
@@ -0,0 +1,481 @@
|
||||
package aprs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidPacket signals a corrupted/unknown APRS packet.
|
||||
ErrInvalidPacket = errors.New("aprs: invalid packet")
|
||||
|
||||
// ErrInvalidPosition signals a corrupted APRS position report.
|
||||
ErrInvalidPosition = errors.New("aprs: invalid position")
|
||||
)
|
||||
|
||||
// Payload is the raw payload contained within an APRS packet.
|
||||
type Payload string
|
||||
|
||||
// Type of payload.
|
||||
func (p Payload) Type() DataType {
|
||||
var t DataType
|
||||
|
||||
if len(p) > 0 {
|
||||
t = DataType(p[0])
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func isDigit(b byte) bool {
|
||||
return b >= '0' && b <= '9'
|
||||
}
|
||||
|
||||
func (p Payload) Len() int { return len(p) }
|
||||
|
||||
// Velocity details.
|
||||
type Velocity struct {
|
||||
Course float64 // Degrees
|
||||
Speed float64 // Knots
|
||||
}
|
||||
|
||||
// Wind details.
|
||||
type Wind struct {
|
||||
Direction float64 // Degrees
|
||||
Speed float64 // Knots
|
||||
}
|
||||
|
||||
// PowerHeightGain details.
|
||||
type PowerHeightGain struct {
|
||||
PowerCode byte
|
||||
HeightCode byte
|
||||
GainCode byte
|
||||
DirectivityCode byte
|
||||
}
|
||||
|
||||
// Power level (in Watts).
|
||||
func (p PowerHeightGain) Power() int {
|
||||
w := int(p.PowerCode - '0')
|
||||
if w <= 0 {
|
||||
return 0
|
||||
}
|
||||
return w * w
|
||||
}
|
||||
|
||||
// Height above ground (in meters).
|
||||
func (p PowerHeightGain) Height() float64 {
|
||||
h := float64(p.HeightCode - '0')
|
||||
if h <= 0 {
|
||||
return 10
|
||||
}
|
||||
return math.Pow(2, h) * 10
|
||||
}
|
||||
|
||||
// Gain level (in dBs).
|
||||
func (p PowerHeightGain) Gain() int {
|
||||
d := int(p.GainCode - '0')
|
||||
if d <= 0 {
|
||||
return 0
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Directivity angle.
|
||||
func (p PowerHeightGain) Directivity() float64 {
|
||||
d := int(p.DirectivityCode - '0')
|
||||
if d <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(d%8) * 45.0
|
||||
}
|
||||
|
||||
// OmniDFStrength contains the omni-directional direction finding signal strength (for fox hunting).
|
||||
type OmniDFStrength struct {
|
||||
StrengthCode byte
|
||||
HeightCode byte
|
||||
GainCode byte
|
||||
DirectivityCode byte
|
||||
}
|
||||
|
||||
// Strength of the signal.
|
||||
func (o OmniDFStrength) Strength() int {
|
||||
w := int(o.StrengthCode - '0')
|
||||
if w <= 0 {
|
||||
return 0
|
||||
}
|
||||
return w * w
|
||||
}
|
||||
|
||||
// Height above ground (in meters).
|
||||
func (o OmniDFStrength) Height() float64 {
|
||||
h := float64(o.HeightCode - '0')
|
||||
if h <= 0 {
|
||||
return 10
|
||||
}
|
||||
return math.Pow(2, h) * 10
|
||||
}
|
||||
|
||||
// Gain level (in dBs).
|
||||
func (o OmniDFStrength) Gain() int {
|
||||
d := int(o.GainCode - '0')
|
||||
if d <= 0 {
|
||||
return 0
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Directivity angle.
|
||||
func (o OmniDFStrength) Directivity() float64 {
|
||||
d := int(o.DirectivityCode - '0')
|
||||
if d <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(d%8) * 45.0
|
||||
}
|
||||
|
||||
// Packet contains an APRS packet.
|
||||
type Packet struct {
|
||||
// Raw packet (as captured from the air or APRS-IS).
|
||||
Raw string `json:"raw"`
|
||||
|
||||
// Src is the source address.
|
||||
Src Address `json:"src"`
|
||||
|
||||
// Dst is the destination address.
|
||||
Dst Address `json:"dst"`
|
||||
|
||||
// Path contains the digipeater path.
|
||||
Path Path `json:"path,omitempty"`
|
||||
|
||||
// Payload is the raw payload.
|
||||
Payload Payload `json:"payload"`
|
||||
|
||||
// Position encoded in the payload.
|
||||
Position *Position `json:"position,omitempty"`
|
||||
|
||||
// Time encoded in the payload.
|
||||
Time *time.Time `json:"time,omitempty"`
|
||||
|
||||
// Altitude encoded in the payload (in feet).
|
||||
Altitude float64 `json:"altitude,omitempty"`
|
||||
|
||||
// Velocity encoded in the payload.
|
||||
Velocity *Velocity `json:"velocity,omitempty"`
|
||||
|
||||
// Wind details encoded in the payload.
|
||||
Wind *Wind `json:"wind,omitempty"`
|
||||
|
||||
// PHG are the power, height and gain details encoded in the payload.
|
||||
PHG *PowerHeightGain `json:"phg,omitempty"`
|
||||
|
||||
// DFS are the direction finder strength details encoded in the payload.
|
||||
DFS *OmniDFStrength `json:"dfs,omitempty"`
|
||||
|
||||
// Range encoded in the payload (in miles).
|
||||
Range float64 `json:"range,omitempty"`
|
||||
|
||||
// Symbol encoded in the payload.
|
||||
Symbol Symbol `json:"symbol"`
|
||||
|
||||
// Comment encoded in the payload.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// Unparsed data.
|
||||
data string
|
||||
}
|
||||
|
||||
// ParsePacket parses an APRS packet as captured from AX.25 or APRS-IS.
|
||||
func ParsePacket(raw string) (Packet, error) {
|
||||
p := Packet{Raw: raw}
|
||||
|
||||
var i int
|
||||
if i = strings.Index(raw, ":"); i < 0 {
|
||||
return p, ErrInvalidPacket
|
||||
}
|
||||
p.Payload = Payload(raw[i+1:])
|
||||
|
||||
// Parse src, dst and path
|
||||
var err error
|
||||
var a = raw[:i]
|
||||
if i = strings.Index(a, ">"); i < 0 {
|
||||
return p, ErrInvalidPacket
|
||||
}
|
||||
if p.Src, err = ParseAddress(a[:i]); err != nil {
|
||||
return p, err
|
||||
}
|
||||
var r = strings.Split(a[i+1:], ",")
|
||||
if p.Dst, err = ParseAddress(r[0]); err != nil {
|
||||
return p, err
|
||||
}
|
||||
if p.Path, err = ParsePath(strings.Join(r[1:], ",")); err != nil {
|
||||
return p, err
|
||||
}
|
||||
|
||||
// Post processing of payload
|
||||
err = p.parse()
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (p *Packet) parse() error {
|
||||
s := string(p.Payload)
|
||||
//log.Printf("parse %q [%c]\n", s, p.Payload.Type())
|
||||
|
||||
switch p.Payload.Type() {
|
||||
case '!': // Lat/Long Position Report Format — without Timestamp
|
||||
var o = strings.IndexByte(s, '!')
|
||||
pos, txt, err := ParsePosition(s[o+1:], !isDigit(s[o+1]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Position = &pos
|
||||
p.data = txt
|
||||
if len(s) >= 20 {
|
||||
p.Symbol[0] = s[9]
|
||||
p.Symbol[1] = s[19]
|
||||
}
|
||||
case '=':
|
||||
compressed := IsValidCompressedSymTable(s[1])
|
||||
pos, txt, err := ParsePosition(s[1:], compressed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Position = &pos
|
||||
p.data = txt
|
||||
if compressed {
|
||||
p.Symbol[0] = s[1]
|
||||
p.Symbol[1] = s[10]
|
||||
} else {
|
||||
p.Symbol[0] = s[9]
|
||||
p.Symbol[1] = s[19]
|
||||
}
|
||||
case '/', '@': // Lat/Long Position Report Format — with Timestamp
|
||||
if len(s) < 8 {
|
||||
return ErrInvalidPosition
|
||||
}
|
||||
|
||||
var compressed bool
|
||||
if s[7] == 'h' || s[7] == 'z' || s[7] == '/' {
|
||||
if ts, err := ParseTime(s[1:]); err == nil {
|
||||
p.Time = &ts
|
||||
}
|
||||
compressed = IsValidCompressedSymTable(s[8])
|
||||
pos, txt, err := ParsePosition(s[8:], compressed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Position = &pos
|
||||
p.data = txt
|
||||
} else if s[7] >= '0' && s[7] <= '9' {
|
||||
ts, err := ParseTime(s[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Time = &ts
|
||||
compressed = IsValidCompressedSymTable(s[10])
|
||||
pos, txt, err := ParsePosition(s[10:], compressed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Position = &pos
|
||||
p.data = txt
|
||||
}
|
||||
if compressed {
|
||||
p.Symbol[0] = s[8]
|
||||
p.Symbol[1] = s[17]
|
||||
} else {
|
||||
p.Symbol[0] = s[16]
|
||||
p.Symbol[1] = s[26]
|
||||
}
|
||||
case ';':
|
||||
pos, txt, err := ParsePosition(s[18:], !isDigit(s[18]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Position = &pos
|
||||
p.data = txt
|
||||
case '[':
|
||||
pos, txt, err := ParsePositionGrid(s[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Position = &pos
|
||||
p.data = txt
|
||||
case '`', '\'':
|
||||
pos, err := ParseMicE(s, p.Dst.Call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Position = &pos
|
||||
p.parseMicEData()
|
||||
|
||||
return nil // there is no additional data to parse
|
||||
default:
|
||||
pos, txt, err := ParsePositionBoth(s)
|
||||
if err != nil {
|
||||
if err != ErrInvalidPosition {
|
||||
return err
|
||||
}
|
||||
p.Comment = s[1:]
|
||||
} else {
|
||||
p.Position = &pos
|
||||
p.Comment = txt
|
||||
}
|
||||
}
|
||||
|
||||
if p.Position != nil {
|
||||
if p.Position.Compressed {
|
||||
return p.parseCompressedData()
|
||||
}
|
||||
return p.parseData()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Packet) parseMicEData() error {
|
||||
// APRS PROTOCOL REFERENCE 1.0.1 Chapter 10, page 42 in PDF
|
||||
|
||||
s := string(p.Payload)
|
||||
|
||||
// Mic-E Message Type
|
||||
var mt []string
|
||||
var t string
|
||||
for i := 0; i < 3; i++ {
|
||||
mc := miceCodes[rune(p.Dst.Call[i])][1]
|
||||
if strings.HasSuffix(mc, "(Custom)") {
|
||||
t = messageTypeCustom
|
||||
} else if strings.HasSuffix(mc, "(Std)") {
|
||||
t = messageTypeStd
|
||||
}
|
||||
mt = append(mt, string(mc[0]))
|
||||
}
|
||||
switch t {
|
||||
case messageTypeStd:
|
||||
mt = append(mt, " (Std)")
|
||||
case messageTypeCustom:
|
||||
mt = append(mt, " (Custom)")
|
||||
}
|
||||
p.Comment = miceMsgTypes[strings.Join(mt, "")]
|
||||
|
||||
// Speed and Course.
|
||||
speed := float64(int(s[4])-28) * 10
|
||||
dc := float64(int(s[5])-28) / 10
|
||||
unit := float64(int(dc))
|
||||
speed += unit
|
||||
course := dc - unit
|
||||
course += float64(int(s[6]) - 28)
|
||||
if speed >= 800 {
|
||||
speed -= 800
|
||||
}
|
||||
speed = 1.852 * speed // convert speed from knots to km/h
|
||||
if course >= 400 {
|
||||
course -= 400
|
||||
}
|
||||
|
||||
// Symbol
|
||||
p.Symbol[0] = s[7]
|
||||
p.Symbol[1] = s[8]
|
||||
|
||||
p.Comment += fmt.Sprintf(" (%.fkm/h, %.f°)", speed, course)
|
||||
|
||||
// Check whether there's additional Telemetry or Status Text data.
|
||||
if len(s) == 9 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s[9] == ',' || s[9] == '\x1d' {
|
||||
// TODO: Parse telemetry data.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse MicE Status Text data.
|
||||
// TODO: Parse additional data in the Status Text data:
|
||||
// - Actual (custom) text message
|
||||
// - Maidenhead locator
|
||||
// - Altitude
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Packet) parseCompressedData() error {
|
||||
// Parse csT bytes
|
||||
if len(p.data) >= 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 |
|
||||
cb := p.data[0] - 33
|
||||
sb := p.data[1] - 33
|
||||
Tb := p.data[2] - 33
|
||||
if p.data[0] != ' ' && ((Tb>>3)&3) == 2 {
|
||||
// CGA sentence, NMEA Source = 0b10
|
||||
d, err := base91Decode(p.data[0:2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Altitude = math.Pow(1.002, float64(d))
|
||||
p.Comment = p.data[3:]
|
||||
} else if cb <= 89 { // !..z
|
||||
// Course/Speed
|
||||
p.Velocity.Course = float64(cb) * 4.0
|
||||
p.Velocity.Speed = math.Pow(1.08, float64(sb)) - 1.0
|
||||
} else if cb == 90 { // {
|
||||
// Pre-Calculated Radio Range
|
||||
p.Range = 2 * math.Pow(1.08, float64(sb))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Packet) parseData() error {
|
||||
switch {
|
||||
case len(p.data) >= 1 && p.data[0] == ' ':
|
||||
p.Comment = p.data[1:]
|
||||
|
||||
case len(p.data) >= 7 && strings.HasPrefix(p.data, "PHG"):
|
||||
p.PHG.PowerCode = p.data[3]
|
||||
p.PHG.HeightCode = p.data[4]
|
||||
p.PHG.GainCode = p.data[5]
|
||||
p.PHG.DirectivityCode = p.data[6]
|
||||
p.Range = math.Sqrt(2 * p.PHG.Height() * math.Sqrt((float64(p.PHG.Power())/10)*(float64(p.PHG.Gain())/2)))
|
||||
p.Comment = p.data[7:]
|
||||
|
||||
case len(p.data) >= 7 && strings.HasPrefix(p.data, "RNG"):
|
||||
var err error
|
||||
p.Range, err = strconv.ParseFloat(p.data[3:7], 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Comment = p.data[7:]
|
||||
|
||||
case len(p.data) >= 7 && strings.HasPrefix(p.data, "DFS"):
|
||||
p.DFS.StrengthCode = p.data[3]
|
||||
p.DFS.HeightCode = p.data[4]
|
||||
p.DFS.GainCode = p.data[5]
|
||||
p.DFS.DirectivityCode = p.data[6]
|
||||
p.Comment = p.data[7:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Payload) Time() (time.Time, error) {
|
||||
switch p.Type() {
|
||||
case '/', '@':
|
||||
return ParseTime(string(p)[1:])
|
||||
default:
|
||||
return time.Time{}, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user