package aprs import ( "fmt" "io" "math" "strings" ) type MicE struct { HasMessaging bool `json:"has_messaging"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` Altitude float64 `json:"altitude,omitempty"` // Altitude (in meters) Comment string `json:"comment"` Symbol string `json:"symbol"` Velocity *Velocity `json:"velocity,omitempty"` // Velocity encoded in the payload. Telemetry *Telemetry `json:"telemetry,omitempty"` // Telemetry data } func (m MicE) String() string { return m.Comment } type micEDecoder struct{} func (d micEDecoder) CanDecode(frame Frame) bool { switch frame.Raw.Type() { case '`', '\'': return len(frame.Raw) >= 9 && len(frame.Destination.Call) >= 6 default: return false } } func (d micEDecoder) Decode(frame Frame) (data Data, err error) { lat, _, longOffset, longDir, err := decodeMicECallsign([]byte(frame.Destination.Call)) if err != nil { return nil, err } info := []byte(frame.Raw[1:9]) long := decodeMicELongitude(info[:3], longOffset, longDir) pos := &MicE{ Latitude: float64(lat), Longitude: float64(long), } pos.Symbol = string([]byte{info[7], info[6]}) pos.Velocity = parseMicECourseAndSpeed(info[3:6]) var comment string if comment, pos.HasMessaging = parseMicERadioModel(string(frame.Raw[9:])); comment != "" { original := comment if pos.Altitude, comment, err = parseMicEAltitude(comment); err != nil { comment = original } if pos.Telemetry, comment, err = parseBase91Telemetry(comment); err != nil { return nil, err } pos.Comment = comment } return pos, nil } type micEMessageBit uint8 const ( micEMessageBitZero micEMessageBit = iota micEMessageBitCustom micEMessageBitStandard micEMessageBitInvalid ) func decodeMicEMessageBit(v uint8) micEMessageBit { switch { case v >= '0' && v <= '9' || v == 'L': return micEMessageBitZero case v >= 'A' && v <= 'K': return micEMessageBitCustom case v >= 'P' && v <= 'Z': return micEMessageBitStandard default: return micEMessageBitInvalid } } type micEPositionCommentType uint8 const ( M0 micEPositionCommentType = iota M1 M2 M3 M4 M5 M6 C0 C1 C2 C3 C4 C5 C6 Emergency Invalid ) var micEPositionCommentTypeMap = map[micEMessageBit]micEPositionCommentType{ (0 << 4) | (0 << 2) | 0: Emergency, (1 << 4) | (1 << 2) | 1: C0, (1 << 4) | (1 << 2) | 0: C1, (1 << 4) | (0 << 2) | 1: C2, (1 << 4) | (0 << 2) | 0: C3, (0 << 4) | (1 << 2) | 1: C4, (0 << 4) | (1 << 2) | 0: C5, (0 << 4) | (0 << 2) | 1: C6, (2 << 4) | (2 << 2) | 2: M0, (2 << 4) | (2 << 2) | 0: M1, (2 << 4) | (0 << 2) | 2: M2, (2 << 4) | (0 << 2) | 0: M3, (0 << 4) | (2 << 2) | 2: M4, (0 << 4) | (2 << 2) | 0: M5, (0 << 4) | (0 << 2) | 2: M6, } func decodeMicEPositionCommentType(bits []micEMessageBit) micEPositionCommentType { if v, ok := micEPositionCommentTypeMap[(bits[0]&0x3)<<4|(bits[1]&0x03)<<2|(bits[2]&0x03)]; ok { return v } return Invalid } func decodeMicECallsign(call []byte) (lat Latitude, kind micEPositionCommentType, longOffset int, longDir int, err error) { if len(call) != 6 { err = io.ErrUnexpectedEOF return } var latDir byte = 'X' if (call[3] >= '0' && call[3] <= '9') || call[3] == 'L' { latDir = 'S' } else if call[3] >= 'P' && call[3] <= 'Z' { latDir = 'N' } latBytes := []byte{ decodeLatitudeDigit(call[0]), decodeLatitudeDigit(call[1]), decodeLatitudeDigit(call[2]), decodeLatitudeDigit(call[3]), '.', decodeLatitudeDigit(call[4]), decodeLatitudeDigit(call[5]), latDir, } if err = lat.ParseUncompressed(latBytes); err != nil { return } kind = decodeMicEPositionCommentType([]micEMessageBit{ decodeMicEMessageBit(call[0]), decodeMicEMessageBit(call[1]), decodeMicEMessageBit(call[2]), }) switch { case (call[4] >= '0' && call[4] <= '9') || call[4] == 'L': longOffset = 0 case call[4] >= 'P' && call[4] <= 'Z': longOffset = 100 } switch { case (call[5] >= '0' && call[5] <= '9') || call[5] == 'L': longDir = -1 case call[5] >= 'P' && call[5] <= 'Z': longDir = +1 } return } func decodeLatitudeDigit(c uint8) uint8 { switch { case c >= '0' && c <= '9': return c case c >= 'A' && c <= 'J': return c - 17 case c == 'K' || c == 'L' || c == 'Z': return ' ' case c >= 'P' && c <= 'Y': return c - 32 default: return 0 } } func decodeMicELongitude(b []byte, offset, dir int) Longitude { if len(b) != 3 { return 0 } d := int(b[0]) - 28 + offset if d >= 180 && d <= 189 { d -= 80 } else if d >= 190 && d <= 199 { d -= 190 } m := int(b[1] - 28) if m >= 60 { m -= 60 } h := int(b[2] - 28) return LongitudeFromDMH(d, m, h, dir < 0) } func parseMicECourseAndSpeed(data []byte) (out *Velocity) { var ( sp = data[0] - 28 dc = data[1] - 28 se = data[2] - 28 speedKnots = float64(sp)*10 + math.Floor(float64(dc)/10) courseDeg = ((int(dc) % 10) * 100) + int(se) ) if speedKnots >= 800 { speedKnots -= 800 } if courseDeg >= 400 { courseDeg -= 400 } return &Velocity{ Course: float64(courseDeg), Speed: knotsToMetersPerSecond(speedKnots), } } func parseMicEAltitude(data string) (altitude float64, comment string, err error) { if len(data) < 4 || data[3] != '}' { return 0, data, nil } var value int if value, err = base91Decode(data[:3]); err != nil { return 0, "", fmt.Errorf("aprs: invalid altitude %q: %v", data, err) } altitude = feetToMeters(float64(value - 10000)) comment = data[4:] return } func parseMicERadioModel(data string) (stripped string, hasMessaging bool) { if len(data) == 0 { return data, false } switch data[0] { case '>', ']': stripped = strings.TrimRight(data[1:], "=") // Kenwood TH-D72 / Kenwood TM-D710 stripped = strings.TrimRight(data[1:], "^") // Kenwood TH-D74 stripped = strings.TrimRight(data[1:], "&") // Kenwood TH-D75 case '`', '\'': hasMessaging = data[0] == '`' stripped = strings.TrimSuffix(data[1:], "_(") // Yaesu FT2D stripped = strings.TrimSuffix(data[1:], "_0") // Yaesu FT3D stripped = strings.TrimSuffix(data[1:], "_3") // Yaesu FT5D stripped = strings.TrimSuffix(data[1:], "|3") // Byonics TinyTrack 3 stripped = strings.TrimSuffix(data[1:], "|4") // Byonics TinyTrack 4 default: stripped = data } return } func parseMicEGridSquare(data string) (latitude, longitude, altitude float64, comment string, err error) { return } var miceCodes = map[byte]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"}, } var 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", }