Added Radio.ID and refactored the Stats interface
This commit is contained in:
158
protocol/adsb/fields/bds05.go
Normal file
158
protocol/adsb/fields/bds05.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
// SurveillanceStatus is the surveillance status
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / A.2.3.2.6
|
||||||
|
type SurveillanceStatus byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
SSNoCondition SurveillanceStatus = iota // No aircraft category information
|
||||||
|
SSPermanentAlert // Permanent alert (emergency condition)
|
||||||
|
SSTemporaryAlert // Temporary alert (change in Mode A identity code other than emergency condition)
|
||||||
|
SSSPICondition // SPI condition
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
// AltitudeSource is the type of source of the Altitude: Barometric or GNSS.
|
||||||
|
type AltitudeSource byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
AltitudeBarometric AltitudeSource = iota // Altitude is barometric altitude
|
||||||
|
AltitudeGNSS // Altitude is GNSS height (HAE)
|
||||||
|
)
|
||||||
|
|
||||||
|
// AltitudeReportMethod defines how the altitude is reported.
|
||||||
|
type AltitudeReportMethod byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
AltitudeReport100FootIncrements AltitudeReportMethod = iota // 100-foot increments
|
||||||
|
AltitudeReport25FootIncrements // 25-foot increments
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompactPositionReportingFormat is the CPR (Compact Position Reporting) format definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / A.2.3.3.3
|
||||||
|
type CompactPositionReportingFormat byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
CPRFormatEven CompactPositionReportingFormat = iota // Even format coding
|
||||||
|
CPRFormatOdd // Odd format coding
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseCompactPositioningReportFormat(data []byte) CompactPositionReportingFormat {
|
||||||
|
bits := (data[2] & 0x04) >> 2
|
||||||
|
return CompactPositionReportingFormat(bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAltitude(data []byte) (altitude int32, source AltitudeSource, method AltitudeReportMethod, err error) {
|
||||||
|
source = AltitudeBarometric
|
||||||
|
if format := (data[0] & 0xF8) >> 3; 20 <= format && format <= 22 {
|
||||||
|
source = AltitudeGNSS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Altitude code is a 12 bits fields, so read a uint16
|
||||||
|
// bit | 8 9 10 11 12 13 14 15| 16 17 18 19 20 21 22 23 |
|
||||||
|
// message | x x x x x x x x| x x x x _ _ _ _ |
|
||||||
|
// 100 foot |C1 A1 C2 A2 C4 A4 B1 Q| B2 D2 B4 D4 _ _ _ _ |
|
||||||
|
// Get the Q bit
|
||||||
|
if qBit := (data[1] & 0x01) != 0; qBit {
|
||||||
|
// If the Q bit equals 1, the 11-bit field represented by bits 8 to 14 and 16 to 18
|
||||||
|
n := uint16(0)
|
||||||
|
n |= uint16(data[1]&0xFE) << 3
|
||||||
|
n |= uint16(data[2]&0xF0) >> 4
|
||||||
|
return 25*int32(n) - 1000, source, AltitudeReport25FootIncrements, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, altitude is reported in 100 foot increments
|
||||||
|
c1 := (data[1] & 0x80) != 0
|
||||||
|
a1 := (data[1] & 0x40) != 0
|
||||||
|
c2 := (data[1] & 0x20) != 0
|
||||||
|
a2 := (data[1] & 0x10) != 0
|
||||||
|
c4 := (data[1] & 0x08) != 0
|
||||||
|
a4 := (data[1] & 0x04) != 0
|
||||||
|
b1 := (data[1] & 0x02) != 0
|
||||||
|
b2 := (data[2] & 0x80) != 0
|
||||||
|
d2 := (data[2] & 0x40) != 0
|
||||||
|
b4 := (data[2] & 0x20) != 0
|
||||||
|
d4 := (data[2] & 0x10) != 0
|
||||||
|
if altitude, err = GillhamToAltitude(false, d2, d4, a1, a2, a4, b1, b2, b4, c1, c2, c4); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MovementStatus is the status of the Movement information
|
||||||
|
type MovementStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MSNoInformation indicates no information
|
||||||
|
MSNoInformation MovementStatus = 0 // No information
|
||||||
|
MSAircraftStopped MovementStatus = 1 // The aircraft is stopped
|
||||||
|
MSAboveMaximum MovementStatus = 124 // The Movement is above the maximum
|
||||||
|
MSReservedDecelerating MovementStatus = 125 // Reserved
|
||||||
|
MSReservedAccelerating MovementStatus = 126 // Reserved
|
||||||
|
MSReservedBackingUp MovementStatus = 127 // Reserved
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseMovementStatus(data []byte) (float64, MovementStatus) {
|
||||||
|
bits := (data[0]&0x07)<<4 + (data[1]&0xF0)>>4
|
||||||
|
status := MovementStatus(bits)
|
||||||
|
return status.Speed(), status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed in knots.
|
||||||
|
func (status MovementStatus) Speed() float64 {
|
||||||
|
if status == 0 || status == 1 || status > 124 {
|
||||||
|
return 0
|
||||||
|
} else if 2 <= status && status <= 8 {
|
||||||
|
return 0.125 + float64(status-2)*0.125
|
||||||
|
} else if 9 <= status && status <= 12 {
|
||||||
|
return 1 + float64(status-9)*0.25
|
||||||
|
} else if 13 <= status && status <= 38 {
|
||||||
|
return 2 + float64(status-13)*0.5
|
||||||
|
} else if 39 <= status && status <= 93 {
|
||||||
|
return 15 + float64(status-39)*1.0
|
||||||
|
} else if 94 <= status && status <= 108 {
|
||||||
|
return 70 + float64(status-94)*2.0
|
||||||
|
} else if 109 <= status && status <= 123 {
|
||||||
|
return 100 + float64(status-109)*5.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movement max
|
||||||
|
return 175
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodedLatitude is the encoded latitude
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / C.2.6
|
||||||
|
type EncodedLatitude uint32
|
||||||
|
|
||||||
|
func ParseEncodedLatitude(data []byte) EncodedLatitude {
|
||||||
|
return EncodedLatitude((data[2]&0x02)>>1)<<16 |
|
||||||
|
EncodedLatitude((data[2]&0x01)<<7+(data[3]&0xFE)>>1)<<8 |
|
||||||
|
EncodedLatitude((data[3]&0x01)<<7+(data[4]&0xFE)>>1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodedLongitude is the encoded longitude
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / C.2.6
|
||||||
|
type EncodedLongitude uint32
|
||||||
|
|
||||||
|
func ParseEncodedLongitude(data []byte) EncodedLongitude {
|
||||||
|
return EncodedLongitude(data[4]&0x01)<<16 |
|
||||||
|
EncodedLongitude(data[5])<<8 |
|
||||||
|
EncodedLongitude(data[6])
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseGroundTrackStatus(data []byte) (track float64, status bool) {
|
||||||
|
status = (data[1] & 0x08) != 0
|
||||||
|
|
||||||
|
allBits := (uint16(data[1]&0x07)<<8 | uint16(data[2]&0xF0)) >> 4
|
||||||
|
track = float64(allBits) * 360.0 / 128.0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseTimeSynchronizedToUTC(data []byte) bool {
|
||||||
|
return ((data[2] & 0x08) >> 3) != 0
|
||||||
|
}
|
||||||
208
protocol/adsb/fields/bds09.go
Normal file
208
protocol/adsb/fields/bds09.go
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
// NumericValueStatus is the status of the numeric values, such as air speed, velocity, etc.
|
||||||
|
type NumericValueStatus byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
NVSNoInformation NumericValueStatus = iota // No velocity information
|
||||||
|
NVSRegular // Velocity is computed on the linear scale value of field * factor
|
||||||
|
NVSMaximum // Velocity field value indicates velocity greater the maximum of the scale
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseHeightDifference reads the Height difference from a 56 bits data field
|
||||||
|
func ParseHeightDifference(data []byte) (int16, NumericValueStatus) {
|
||||||
|
negative := data[6]&0x80 != 0
|
||||||
|
difference := int16(data[6] & 0x7F)
|
||||||
|
|
||||||
|
if difference == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if difference >= 127 {
|
||||||
|
if negative {
|
||||||
|
return -3150, NVSMaximum
|
||||||
|
} else {
|
||||||
|
return 3150, NVSMaximum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
difference = (difference - 1) * 25
|
||||||
|
if negative {
|
||||||
|
difference = -difference
|
||||||
|
}
|
||||||
|
|
||||||
|
return difference, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerticalRateSource is the Source Bit for Vertical Rate definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / Table A-2-9
|
||||||
|
type VerticalRateSource byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
VerticalRateSourceGNSS VerticalRateSource = iota // GNSS source
|
||||||
|
VerticalRateSourceBarometric // Barometric source
|
||||||
|
)
|
||||||
|
|
||||||
|
// DirectionNorthSouth is the Direction Bit NS Velocity definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / Table A-2-9
|
||||||
|
type DirectionNorthSouth byte
|
||||||
|
|
||||||
|
func ParseDirectionNorthSouth(data []byte) DirectionNorthSouth {
|
||||||
|
return DirectionNorthSouth((data[3] & 0x80) >> 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVelocityNorthSouthNormal(data []byte) (velocity uint16, status NumericValueStatus) {
|
||||||
|
velocity = (uint16(data[3]&0x7F)<<8 | uint16(data[4]&0xE0)) >> 5
|
||||||
|
if velocity == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if velocity >= 1023 {
|
||||||
|
return 1023, NVSMaximum
|
||||||
|
}
|
||||||
|
return velocity - 1, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVelocityNorthSouthSupersonic(data []byte) (velocity uint16, status NumericValueStatus) {
|
||||||
|
velocity = (uint16(data[3]&0x7F)<<8 | uint16(data[4]&0xE0)) >> 5
|
||||||
|
if velocity == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if velocity >= 1023 {
|
||||||
|
return 4088, NVSMaximum
|
||||||
|
}
|
||||||
|
return (velocity - 1) * 4, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DNSNorth DirectionNorthSouth = iota // North
|
||||||
|
DNSSouth // South
|
||||||
|
)
|
||||||
|
|
||||||
|
// DirectionEastWest is the Direction Bit EW Velocity definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / Table A-2-9
|
||||||
|
type DirectionEastWest byte
|
||||||
|
|
||||||
|
func ParseDirectionEastWest(data []byte) DirectionEastWest {
|
||||||
|
return DirectionEastWest((data[1] & 0x04) >> 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVelocityEastWestNormal(data []byte) (velocity uint16, status NumericValueStatus) {
|
||||||
|
velocity = (uint16(data[1]&0x03) | uint16(data[2]))
|
||||||
|
if velocity == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if velocity >= 1023 {
|
||||||
|
return 1023, NVSMaximum
|
||||||
|
}
|
||||||
|
return velocity - 1, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVelocityEastWestSupersonic(data []byte) (velocity uint16, status NumericValueStatus) {
|
||||||
|
velocity = (uint16(data[1]&0x03) | uint16(data[2]))
|
||||||
|
if velocity == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if velocity >= 1023 {
|
||||||
|
return 4088, NVSMaximum
|
||||||
|
}
|
||||||
|
return (velocity - 1) * 4, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEWEast DirectionEastWest = iota // East
|
||||||
|
DEWWest // West
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseIntentChange(data []byte) bool {
|
||||||
|
return (data[1]&0x80)>>7 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIFRCapability(data []byte) bool {
|
||||||
|
return (data[1]&0x40)>>6 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// NavigationUncertaintyCategory is the Navigation Uncertainty Category definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / Table A-2-9
|
||||||
|
type NavigationUncertaintyCategory byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
NUCPUnknown NavigationUncertaintyCategory = iota // Unknown
|
||||||
|
NUCPHorizontalLowerThan10VerticalLowerThan15Point2 // Horizontal < 10m/s and Vertical < 15.2m/s
|
||||||
|
NUCPHorizontalLowerThan3VerticalLowerThan4Point6 // Horizontal < 3m/s and Vertical < 4.6m/s
|
||||||
|
NUCPHorizontalLowerThan1VerticalLowerThan1Point5 // Horizontal < 1m/s and Vertical < 1.5m/s
|
||||||
|
NUCPHorizontalLowerThan0Point3VerticalLowerThan0Point46 // Horizontal < 0.3m/s and Vertical < 0.46m/s
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseeNavigationUncertaintyCategory(data []byte) NavigationUncertaintyCategory {
|
||||||
|
return NavigationUncertaintyCategory((data[1] & 0x38) >> 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMagneticHeading(data []byte) (heading float64, status bool) {
|
||||||
|
status = (data[1]&0x04)>>2 != 0
|
||||||
|
value := uint16(data[1]&0x03)<<8 | uint16(data[2])
|
||||||
|
heading = float64(value) * 360 / 1024.0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAirspeedNormal(data []byte) (speed uint16, status NumericValueStatus) {
|
||||||
|
velocity := (uint16(data[3]&0x7F)<<8 | uint16(data[4]&0xE0)) >> 5
|
||||||
|
if velocity == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if velocity >= 1023 {
|
||||||
|
return 1023, NVSMaximum
|
||||||
|
}
|
||||||
|
return velocity - 1, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAirspeedSupersonic(data []byte) (speed uint16, status NumericValueStatus) {
|
||||||
|
velocity := (uint16(data[3]&0x7F)<<8 | uint16(data[4]&0xE0)) >> 5
|
||||||
|
if velocity == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if velocity >= 1023 {
|
||||||
|
return 4088, NVSMaximum
|
||||||
|
}
|
||||||
|
return (velocity - 1) * 4, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVerticalRateSource(data []byte) VerticalRateSource {
|
||||||
|
return VerticalRateSource((data[4] & 0x10) >> 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVerticalRate(data []byte) (rate int16, status NumericValueStatus) {
|
||||||
|
negative := data[4]&0x08 != 0
|
||||||
|
rate = int16(uint16(data[4]&0x07)<<8 | uint16(data[5]&0xFC))
|
||||||
|
if rate == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if rate >= 511 {
|
||||||
|
if negative {
|
||||||
|
return -32640, NVSMaximum
|
||||||
|
}
|
||||||
|
return 32640, NVSMaximum
|
||||||
|
}
|
||||||
|
|
||||||
|
rate = (rate - 1) * 64
|
||||||
|
if negative {
|
||||||
|
rate = -rate
|
||||||
|
}
|
||||||
|
return rate, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHeightDifferenceFromBaro(data []byte) (difference int16, status NumericValueStatus) {
|
||||||
|
negative := data[6]&0x80 != 0
|
||||||
|
difference = int16(data[6] & 0x7F)
|
||||||
|
|
||||||
|
if difference == 0 {
|
||||||
|
return 0, NVSNoInformation
|
||||||
|
} else if difference >= 127 {
|
||||||
|
if negative {
|
||||||
|
return -3150, NVSMaximum
|
||||||
|
} else {
|
||||||
|
return 3150, NVSMaximum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
difference = (difference - 1) * 25
|
||||||
|
if negative {
|
||||||
|
difference = -difference
|
||||||
|
}
|
||||||
|
|
||||||
|
return difference, NVSRegular
|
||||||
|
}
|
||||||
21
protocol/adsb/fields/bds61.go
Normal file
21
protocol/adsb/fields/bds61.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
// EmergencyPriorityStatus is the Emergency Priority Status definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / Table B-2-97a
|
||||||
|
type EmergencyPriorityStatus byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
EPSNoEmergency EmergencyPriorityStatus = iota // No emergency
|
||||||
|
EPSGeneralEmergency // General emergency
|
||||||
|
EPSLifeguardMedical // Lifeguard/medical emergency
|
||||||
|
EPSMinimumFuel // Minimum fuel
|
||||||
|
EPSNoCommunication // No communications
|
||||||
|
EPSUnlawfulInterference // Unlawful interference
|
||||||
|
EPSDownedAircraft // Downed aircraft
|
||||||
|
EPSReserved7 // Reserved
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseEmergencyPriorityStatus(data []byte) EmergencyPriorityStatus {
|
||||||
|
return EmergencyPriorityStatus((data[1] & 0xE0) >> 5)
|
||||||
|
}
|
||||||
176
protocol/adsb/fields/bds62.go
Normal file
176
protocol/adsb/fields/bds62.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
// VerticalDataAvailableSourceIndicator is the Vertical Data Available / Source Indicator definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.3
|
||||||
|
type VerticalDataAvailableSourceIndicator byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
VDANoValidDataAvailable VerticalDataAvailableSourceIndicator = iota // No data available
|
||||||
|
VDAAutopilot // Autopilot control panel selected value, such as Mode Control Panel (MCP) or Flight Control Unit (FCU)
|
||||||
|
VDAHoldingAltitude // Holding altitude
|
||||||
|
VDAFMS // FMS/RNAV system
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseVerticalDataAvailableSourceIndicator(data []byte) VerticalDataAvailableSourceIndicator {
|
||||||
|
return VerticalDataAvailableSourceIndicator((data[0]&0x01)<<1 + (data[1]&0x80)>>7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetAltitudeType is the Target Altitude Type definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.4
|
||||||
|
type TargetAltitudeType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
TATReferencedToPressureAltitude TargetAltitudeType = iota // Referenced to pressure-altitude (flight level)
|
||||||
|
TATReferencedToBarometricAltitude // Referenced to barometric corrected altitude (mean sea level)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadTargetAltitudeType reads the TargetAltitudeType from a 56 bits data field
|
||||||
|
func ParseTargetAltitudeType(data []byte) TargetAltitudeType {
|
||||||
|
return TargetAltitudeType((data[1] & 0x40) >> 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetAltitudeCapability is the Target Altitude Capability definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.5
|
||||||
|
type TargetAltitudeCapability byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
TACAltitudeOnly TargetAltitudeCapability = iota // Holding altitude only
|
||||||
|
TACAltitudeOrAutopilot // Either holding altitude or autopilot control panel selected altitude
|
||||||
|
TACAltitudeOrAutopilotOrFMS // Either holding altitude, autopilot control panel selected altitude, or any FMS/RNAV level-off altitude
|
||||||
|
TACReserved3 // Reserved
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseTargetAltitudeCapability reads the TargetAltitudeCapability from a 56 bits data field
|
||||||
|
func ParseTargetAltitudeCapability(data []byte) TargetAltitudeCapability {
|
||||||
|
return TargetAltitudeCapability((data[1] & 0x18) >> 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerticalModeIndicator is the Vertical Mode Indicator definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.6
|
||||||
|
type VerticalModeIndicator byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
VMIUnknown VerticalModeIndicator = iota // Unknown mode or information unavailable
|
||||||
|
VMIAcquiringMode // Acquiring Mode
|
||||||
|
VMICapturingMode // Capturing or Maintaining Mode
|
||||||
|
VMIReserved3 // Reserved
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseVerticalModeIndicator reads the VerticalModeIndicator from a 56 bits data field
|
||||||
|
func ParseVerticalModeIndicator(data []byte) VerticalModeIndicator {
|
||||||
|
return VerticalModeIndicator((data[1] & 0x06) >> 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HorizontalDataAvailableSourceIndicator is the Horizontal Data Available / Source Indicator definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.8
|
||||||
|
type HorizontalDataAvailableSourceIndicator byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
HDANoValidDataAvailable HorizontalDataAvailableSourceIndicator = iota // No data available
|
||||||
|
HDAAutopilot // Autopilot control panel selected value, such as Mode Control Panel (MCP) or Flight Control Unit (FCU)
|
||||||
|
HDAHoldingAltitude // Maintaining current heading or track angle (e.g. autopilot mode select)
|
||||||
|
HDAFMS // FMS/RNAV system (indicates track angle specified by leg type)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseHorizontalDataAvailableSourceIndicator reads the HorizontalDataAvailableSourceIndicator from a 56 bits data field
|
||||||
|
func ParseHorizontalDataAvailableSourceIndicator(data []byte) HorizontalDataAvailableSourceIndicator {
|
||||||
|
return HorizontalDataAvailableSourceIndicator((data[3] & 0x60) >> 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetHeadingTrackIndicator is the Target Heading / Track Angle Indicator definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.10
|
||||||
|
type TargetHeadingTrackIndicator byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
TargetTrackHeadingAngle TargetHeadingTrackIndicator = iota // Target heading angle is being reported
|
||||||
|
TargetTrackAngle // Track angle is being reported
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseTargetHeadingTrackIndicator reads the TargetHeadingTrackIndicator from a 56 bits data field
|
||||||
|
func ParseTargetHeadingTrackIndicator(data []byte) TargetHeadingTrackIndicator {
|
||||||
|
return TargetHeadingTrackIndicator((data[4] & 0x08) >> 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadTargetHeadingTrackAngle reads the TargetAltitude from a 56 bits data field
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.9
|
||||||
|
func ReadTargetHeadingTrackAngle(data []byte) (uint16, NumericValueStatus) {
|
||||||
|
heading := (uint16(data[3]&0x1F)<<8 | uint16(data[2]&0xF0)) >> 4
|
||||||
|
if heading > 359 {
|
||||||
|
return 0, NVSMaximum
|
||||||
|
}
|
||||||
|
return heading, NVSRegular
|
||||||
|
}
|
||||||
|
|
||||||
|
// HorizontalModeIndicator is the Horizontal Mode Indicator definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.11
|
||||||
|
type HorizontalModeIndicator byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
HMIUnknown HorizontalModeIndicator = iota // Unknown mode or information unavailable
|
||||||
|
HMIAcquiringMode // Acquiring Mode
|
||||||
|
HMICapturingMode // Capturing or Maintaining Mode
|
||||||
|
HMIReserved3 // Reserved
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseHorizontalModeIndicator reads the HorizontalModeIndicator from a 56 bits data field
|
||||||
|
func ParseHorizontalModeIndicator(data []byte) HorizontalModeIndicator {
|
||||||
|
return HorizontalModeIndicator((data[4] & 0x06) >> 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NavigationalAccuracyCategoryPositionV1 is the Navigational Accuracy Category Position definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.12
|
||||||
|
type NavigationalAccuracyCategoryPositionV1 byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
NACPV1EPUGreaterThan18Point52Km NavigationalAccuracyCategoryPositionV1 = iota // EPU >= 18.52 km (10 NM) - Unknown accuracy
|
||||||
|
NACPV1EPULowerThan18Point52Km // EPU < 18.52 km (10 NM) - RNP-10 accuracy
|
||||||
|
NACPV1EPULowerThan7Point408Km // EPU < 7.408 km (4 NM) - RNP-4 accuracy
|
||||||
|
NACPV1EPULowerThan3Point704Km // EPU < 3.704 km (2 NM) - RNP-2 accuracy
|
||||||
|
NACPV1EPUGreaterThan1852M // EPU < 1 852 m (1 NM) - RNP-1 accuracy
|
||||||
|
NACPV1EPULowerThan926M // EPU < 926 m (0.5 NM) - RNP-0.5 accuracy
|
||||||
|
NACPV1EPUGreaterThan555Point6M // EPU < 555.6 m ( 0.3 NM) - RNP-0.3 accuracy
|
||||||
|
NACPV1EPULowerThan185Point2M // EPU < 185.2 m (0.1 NM) - RNP-0.1 accuracy
|
||||||
|
NACPV1EPUGreaterThan92Point6M // EPU < 92.6 m (0.05 NM) - e.g. GPS (with SA)
|
||||||
|
NACPV1EPULowerThan30MAndVEPULowerThan45M // EPU < 30 m and VEPU < 45 m - e.g. GPS (SA off)
|
||||||
|
NACPV1EPULowerThan10MAndVEPULowerThan15M // EPU < 10 m and VEPU < 15 m - e.g. WAAS
|
||||||
|
NACPV1EPULowerThan4MAndVEPULowerThan3M // EPU < 3 m and VEPU < 4 m - e.g. LAAS
|
||||||
|
NACPV1Reserved12 // Reserved
|
||||||
|
NACPV1Reserved13 // Reserved
|
||||||
|
NACPV1Reserved14 // Reserved
|
||||||
|
NACPV1Reserved15 // Reserved
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseNavigationalAccuracyCategoryPositionV1 reads the NavigationalAccuracyCategoryPositionV1 from a 56 bits data field
|
||||||
|
func ParseNavigationalAccuracyCategoryPositionV1(data []byte) NavigationalAccuracyCategoryPositionV1 {
|
||||||
|
return NavigationalAccuracyCategoryPositionV1((data[4]&0x01)<<3 + (data[5]&0xE0)>>5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NICBaro is the NIC Baro definition
|
||||||
|
//
|
||||||
|
// Specified in Doc 9871 / B.2.3.9.13
|
||||||
|
type NICBaro byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NICBGilhamNotCrossChecked indicates that the barometric altitude that is being reported in the Airborne
|
||||||
|
// Position Message is based on a Gilham coded input that has not been cross-checked against another source of
|
||||||
|
// pressure-altitude
|
||||||
|
NICBGilhamNotCrossChecked NICBaro = iota
|
||||||
|
// NICBGilhamCrossCheckedOrNonGilham indicates that the barometric altitude that is being reported in the Airborne
|
||||||
|
// Position Message is either based on a Gilham code input that has been cross-checked against another source of
|
||||||
|
// pressure-altitude and verified as being consistent, or is based on a non-Gilham coded source
|
||||||
|
NICBGilhamCrossCheckedOrNonGilham
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseNICBaro reads the NICBaro from a 56 bits data field
|
||||||
|
func ParseNICBaro(data []byte) NICBaro {
|
||||||
|
return NICBaro((data[5] & 0x10) >> 4)
|
||||||
|
}
|
||||||
78
protocol/adsb/fields/util.go
Normal file
78
protocol/adsb/fields/util.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// GillhamToAltitude convert an altitude given in Gillham bits to an altitude in feet.
|
||||||
|
func GillhamToAltitude(d1, d2, d4, a1, a2, a4, b1, b2, b4, c1, c2, c4 bool) (int32, error) {
|
||||||
|
fiveHundredBits := uint16(0)
|
||||||
|
if d1 {
|
||||||
|
fiveHundredBits |= 0x0100
|
||||||
|
}
|
||||||
|
if d2 {
|
||||||
|
fiveHundredBits |= 0x0080
|
||||||
|
}
|
||||||
|
if d4 {
|
||||||
|
fiveHundredBits |= 0x0040
|
||||||
|
}
|
||||||
|
if a1 {
|
||||||
|
fiveHundredBits |= 0x0020
|
||||||
|
}
|
||||||
|
if a2 {
|
||||||
|
fiveHundredBits |= 0x0010
|
||||||
|
}
|
||||||
|
if a4 {
|
||||||
|
fiveHundredBits |= 0x0008
|
||||||
|
}
|
||||||
|
if b1 {
|
||||||
|
fiveHundredBits |= 0x0004
|
||||||
|
}
|
||||||
|
if b2 {
|
||||||
|
fiveHundredBits |= 0x0002
|
||||||
|
}
|
||||||
|
if b4 {
|
||||||
|
fiveHundredBits |= 0x0001
|
||||||
|
}
|
||||||
|
|
||||||
|
oneHundredBits := uint16(0)
|
||||||
|
if c1 {
|
||||||
|
oneHundredBits |= 0x0004
|
||||||
|
}
|
||||||
|
if c2 {
|
||||||
|
oneHundredBits |= 0x0002
|
||||||
|
}
|
||||||
|
if c4 {
|
||||||
|
oneHundredBits |= 0x0001
|
||||||
|
}
|
||||||
|
|
||||||
|
oneHundred := int32(grayToBinary(oneHundredBits))
|
||||||
|
fiveHundred := int32(grayToBinary(fiveHundredBits))
|
||||||
|
|
||||||
|
// Check for invalid codes.
|
||||||
|
if oneHundred == 5 || oneHundred == 6 || oneHundred == 0 {
|
||||||
|
return 0, errors.New("the bits C1 to to C4 are incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 7s from OneHundreds.
|
||||||
|
if oneHundred == 7 {
|
||||||
|
oneHundred = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct order of OneHundreds.
|
||||||
|
if fiveHundred%2 != 0 {
|
||||||
|
oneHundred = 6 - oneHundred
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to feet and apply altitude datum offset.
|
||||||
|
return (int32(fiveHundred)*500 + int32(oneHundred)*100) - 1300, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func grayToBinary(num uint16) uint16 {
|
||||||
|
temp := uint16(0)
|
||||||
|
|
||||||
|
temp = num ^ (num >> 8)
|
||||||
|
temp ^= temp >> 4
|
||||||
|
temp ^= temp >> 2
|
||||||
|
temp ^= temp >> 1
|
||||||
|
|
||||||
|
return temp
|
||||||
|
}
|
||||||
52
protocol/adsb/fields/util_test.go
Normal file
52
protocol/adsb/fields/util_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGillhamToAltitude(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
Test string
|
||||||
|
Want int32
|
||||||
|
}{
|
||||||
|
{"00000000010", -1000},
|
||||||
|
{"00000001010", -500},
|
||||||
|
{"00000011010", 0},
|
||||||
|
{"00000011110", 100},
|
||||||
|
{"00000010011", 600},
|
||||||
|
{"00000110010", 1000},
|
||||||
|
{"00001001001", 5800},
|
||||||
|
{"00011100100", 10300},
|
||||||
|
{"01100011010", 32000},
|
||||||
|
{"01110000100", 46300},
|
||||||
|
{"10000000011", 126600},
|
||||||
|
{"10000000001", 126700},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.Test, func(t *testing.T) {
|
||||||
|
alt, err := GillhamToAltitude(testString2Bits(test.Test))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
if alt != test.Want {
|
||||||
|
t.Errorf("expected altitude %d, got %d", test.Want, alt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testString2Bits(s string) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {
|
||||||
|
d2 := s[0] == '1'
|
||||||
|
d4 := s[1] == '1'
|
||||||
|
a1 := s[2] == '1'
|
||||||
|
a2 := s[3] == '1'
|
||||||
|
a4 := s[4] == '1'
|
||||||
|
b1 := s[5] == '1'
|
||||||
|
b2 := s[6] == '1'
|
||||||
|
b4 := s[7] == '1'
|
||||||
|
c1 := s[8] == '1'
|
||||||
|
c2 := s[9] == '1'
|
||||||
|
c4 := s[10] == '1'
|
||||||
|
|
||||||
|
return false, d2, d4, a1, a2, a4, b1, b2, b4, c1, c2, c4
|
||||||
|
}
|
||||||
527
protocol/adsb/message.go
Normal file
527
protocol/adsb/message.go
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
package adsb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.maze.io/go/ham/protocol/adsb/fields"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrLength = errors.New("adsb: the supplied data must be 7 bytes long")
|
||||||
|
ErrFormatType = errors.New("adsb: invalid format type code")
|
||||||
|
ErrSubType = errors.New("adsb: invalid sub type code")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Payload interface {
|
||||||
|
UnmarshalBytes([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message []byte
|
||||||
|
|
||||||
|
// FormatType returns the format type code
|
||||||
|
//
|
||||||
|
// Code BDS Description V0 V1 V2
|
||||||
|
//
|
||||||
|
// 0 ? No Position
|
||||||
|
// 1 0,8 Aircraft Id OK OK OK
|
||||||
|
// 2 0,8 Aircraft Id OK OK OK
|
||||||
|
// 3 0,8 Aircraft Id OK OK OK
|
||||||
|
// 4 0,8 Aircraft Id OK OK OK
|
||||||
|
// 5 0,6 Surface position OK OK OK
|
||||||
|
// 6 0,6 Surface position OK OK OK
|
||||||
|
// 7 0,6 Surface position OK OK OK
|
||||||
|
// 8 0,6 Surface position OK OK OK
|
||||||
|
// 9 0,5 Airborne position OK OK OK
|
||||||
|
//
|
||||||
|
// 10 0,5 Airborne position OK OK OK
|
||||||
|
// 14 0,5 Airborne position OK OK OK
|
||||||
|
// 12 0,5 Airborne position OK OK OK
|
||||||
|
// 13 0,5 Airborne position OK OK OK
|
||||||
|
// 14 0,5 Airborne position OK OK OK
|
||||||
|
// 15 0,5 Airborne position OK OK OK
|
||||||
|
// 16 0,5 Airborne position OK OK OK
|
||||||
|
// 17 0,5 Airborne position OK OK OK
|
||||||
|
// 18 0,5 Airborne position OK OK OK
|
||||||
|
// 19 0,9 Airborne velocity OK OK OK
|
||||||
|
// 20 0,5 Airborne position OK OK OK
|
||||||
|
// 21 0,5 Airborne position OK OK OK
|
||||||
|
// 22 0,5 Airborne position OK OK OK
|
||||||
|
// 23 Reserved
|
||||||
|
// 24 Reserved
|
||||||
|
// 25 Reserved
|
||||||
|
// 26 Reserved
|
||||||
|
// 27 Reserved
|
||||||
|
// 28 6,1 Emergency report OK OK OK
|
||||||
|
// 29 6,2 Target and status __ OK OK
|
||||||
|
// 30 Reserved
|
||||||
|
// 31 6,5 Operational status OK OK OK
|
||||||
|
func (msg Message) FormatType() byte {
|
||||||
|
if len(msg) < 7 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (msg[0] & 0xF8) >> 3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg Message) Payload() (Payload, error) {
|
||||||
|
var payload Payload
|
||||||
|
switch formatType := msg.FormatType(); formatType {
|
||||||
|
case 0:
|
||||||
|
if msg[2]&0x0F == 0 && msg[3] == 0 && msg[4] == 0 && msg[5] == 0 && msg[6] == 0 {
|
||||||
|
payload = new(NoPositionInformation)
|
||||||
|
} else {
|
||||||
|
payload = new(AirbornePositionType0)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1, 2, 3, 4:
|
||||||
|
payload = new(AircraftIdentificationAndCategory)
|
||||||
|
|
||||||
|
case 5, 6, 7, 8:
|
||||||
|
payload = new(SurfacePosition)
|
||||||
|
|
||||||
|
case 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 21, 22:
|
||||||
|
payload = new(AirbornePosition)
|
||||||
|
|
||||||
|
case 19:
|
||||||
|
// check sub type
|
||||||
|
switch subType := msg[0] & 0x07; subType {
|
||||||
|
case 1:
|
||||||
|
payload = new(AirborneVelocityAirSpeedNormal)
|
||||||
|
case 2:
|
||||||
|
payload = new(AirborneVelocityAirSpeedSupersonic)
|
||||||
|
case 3:
|
||||||
|
payload = new(AirborneVelocityGroundSpeedNormal)
|
||||||
|
case 4:
|
||||||
|
payload = new(AirborneVelocityGroundSpeedSupersonic)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("adsb: invalid airborne velocity sub type %d", subType)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 28:
|
||||||
|
switch subType := msg[0] & 0x07; subType {
|
||||||
|
case 0:
|
||||||
|
payload = new(AircraftStatusNoInformation)
|
||||||
|
case 1:
|
||||||
|
payload = new(AircraftStatusEmergency)
|
||||||
|
case 2:
|
||||||
|
payload = new(AircraftStatusACAS)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("adsb: invalid aircraft status sub type %d", subType)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 29:
|
||||||
|
switch subType := (msg[0] & 0x06) >> 1; subType {
|
||||||
|
case 0:
|
||||||
|
payload = new(TargetStateAndStatus0)
|
||||||
|
case 1:
|
||||||
|
payload = new(TargetStateAndStatus1)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("adsb: invalid target state and status sub type %d", subType)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 31:
|
||||||
|
payload = new(AircraftOperationalStatus)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("adsb: the format type code %d is not supported", formatType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := payload.UnmarshalBytes(msg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoPositionInformation struct {
|
||||||
|
FormatType byte
|
||||||
|
AltitudeBarometric int32
|
||||||
|
NavigationIntegrityCategory byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *NoPositionInformation) UnmarshalBytes(data []byte) error {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return ErrLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the first byte to have a standard BDS code 0,5 (Airborne position with baro altitude) - format 15
|
||||||
|
temp := []byte{0x78, data[1], data[2], 0, 0, 0, 0}
|
||||||
|
var pos AirbornePosition
|
||||||
|
if err := pos.UnmarshalBytes(temp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.FormatType = pos.FormatType
|
||||||
|
msg.AltitudeBarometric = pos.AltitudeInFeet
|
||||||
|
msg.NavigationIntegrityCategory = 0
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AirbornePosition is a message at the format BDS 0,5
|
||||||
|
type AirbornePosition struct {
|
||||||
|
FormatType byte
|
||||||
|
SurveillanceStatus fields.SurveillanceStatus
|
||||||
|
HasDualAntenna bool
|
||||||
|
AltitudeSource fields.AltitudeSource
|
||||||
|
AltitudeReportMethod fields.AltitudeReportMethod
|
||||||
|
AltitudeInFeet int32
|
||||||
|
TimeSynchronizedToUTC bool
|
||||||
|
CompactPositionReportingFormat fields.CompactPositionReportingFormat
|
||||||
|
EncodedLatitude uint32
|
||||||
|
EncodedLongitude uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AirbornePosition) UnmarshalBytes(data []byte) error {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return ErrLength
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.FormatType = Message(data).FormatType()
|
||||||
|
if msg.FormatType < 9 || msg.FormatType > 22 || msg.FormatType == 19 {
|
||||||
|
return ErrFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if msg.AltitudeInFeet, msg.AltitudeSource, msg.AltitudeReportMethod, err = fields.ParseAltitude(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirbornePositionType0 struct {
|
||||||
|
FormatTypeCode byte
|
||||||
|
NavigationIntegrityCategory byte
|
||||||
|
AirbornePosition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AirbornePositionType0) UnmarshalBytes(data []byte) error {
|
||||||
|
// Update the first byte to have a standard BDS code 0,5 (Airborne position with baro altitude) - format 22
|
||||||
|
temp := []byte{0xB0, data[1], data[2], data[3], data[4], data[5], data[6]}
|
||||||
|
|
||||||
|
if err := msg.AirbornePosition.UnmarshalBytes(temp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.NavigationIntegrityCategory = 0
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AircraftIdentificationAndCategory struct {
|
||||||
|
FormatType byte
|
||||||
|
CategorySet byte
|
||||||
|
Category byte
|
||||||
|
Identification string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AircraftIdentificationAndCategory) UnmarshalBytes(data []byte) error {
|
||||||
|
if len(data) < 7 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.FormatType = Message(data).FormatType()
|
||||||
|
for msg.FormatType < 1 || msg.FormatType > 4 {
|
||||||
|
return ErrFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Category = data[0] & 0x07
|
||||||
|
switch msg.FormatType {
|
||||||
|
case 1:
|
||||||
|
msg.CategorySet = 'D'
|
||||||
|
case 2:
|
||||||
|
msg.CategorySet = 'C'
|
||||||
|
case 3:
|
||||||
|
msg.CategorySet = 'B'
|
||||||
|
default:
|
||||||
|
msg.CategorySet = 'A'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the codes
|
||||||
|
codes := make([]byte, 8)
|
||||||
|
codes[0] = (data[1] & 0xFC) >> 2
|
||||||
|
codes[1] = (data[1]&0x03)<<4 + (data[2]&0xF0)>>4
|
||||||
|
codes[2] = (data[2]&0x0F)<<2 + (data[3]&0xC0)>>6
|
||||||
|
codes[3] = data[3] & 0x3F
|
||||||
|
codes[4] = (data[4] & 0xFC) >> 2
|
||||||
|
codes[5] = (data[4]&0x03)<<4 + (data[5]&0xF0)>>4
|
||||||
|
codes[6] = (data[5]&0x0F)<<2 + (data[6]&0xC0)>>6
|
||||||
|
codes[7] = data[6] & 0x3F
|
||||||
|
|
||||||
|
// Convert the codes to actual char
|
||||||
|
chars := make([]byte, 8)
|
||||||
|
for i, code := range codes {
|
||||||
|
chars[i] = identificationCharacterCoding[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Identification = string(chars)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SurfacePosition struct {
|
||||||
|
FormatType byte
|
||||||
|
MovementStatus fields.MovementStatus
|
||||||
|
MovementSpeed float64
|
||||||
|
GroundTrackStatus bool
|
||||||
|
GroundTrack float64
|
||||||
|
TimeSynchronizedToUTC bool
|
||||||
|
CompactPositionReportingFormat fields.CompactPositionReportingFormat
|
||||||
|
EncodedLatitude fields.EncodedLatitude
|
||||||
|
EncodedLongitude fields.EncodedLongitude
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *SurfacePosition) UnmarshalBytes(data []byte) error {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return ErrLength
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.FormatType = (data[0] & 0xF8) >> 3
|
||||||
|
if msg.FormatType < 5 || msg.FormatType > 8 {
|
||||||
|
return ErrFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.MovementSpeed, msg.MovementStatus = fields.ParseMovementStatus(data)
|
||||||
|
msg.GroundTrack, msg.GroundTrackStatus = fields.ParseGroundTrackStatus(data)
|
||||||
|
msg.TimeSynchronizedToUTC = fields.ParseTimeSynchronizedToUTC(data)
|
||||||
|
msg.CompactPositionReportingFormat = fields.ParseCompactPositioningReportFormat(data)
|
||||||
|
msg.EncodedLatitude = fields.ParseEncodedLatitude(data)
|
||||||
|
msg.EncodedLongitude = fields.ParseEncodedLongitude(data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirborneVelocity struct {
|
||||||
|
FormatType byte
|
||||||
|
SubType byte
|
||||||
|
IntentChange bool
|
||||||
|
IFRCapability bool
|
||||||
|
NavigationUncertaintyCategory fields.NavigationUncertaintyCategory
|
||||||
|
MagneticHeadingStatus bool
|
||||||
|
MagneticHeading float64
|
||||||
|
AirspeedStatus fields.NumericValueStatus
|
||||||
|
Airspeed uint16
|
||||||
|
VerticalRateSource fields.VerticalRateSource
|
||||||
|
VerticalRateStatus fields.NumericValueStatus
|
||||||
|
VerticalRate int16
|
||||||
|
}
|
||||||
|
|
||||||
|
// AirborneVelocityGroundSpeedNormal is a message at the format BDS 9,0
|
||||||
|
type AirborneVelocityGroundSpeedNormal struct {
|
||||||
|
AirborneVelocity
|
||||||
|
DirectionEastWest fields.DirectionEastWest
|
||||||
|
VelocityEWStatus fields.NumericValueStatus
|
||||||
|
VelocityEW uint16
|
||||||
|
DirectionNorthSouth fields.DirectionNorthSouth
|
||||||
|
VelocityNSStatus fields.NumericValueStatus
|
||||||
|
VelocityNS uint16
|
||||||
|
DifferenceAltitudeGNSSBaroStatus fields.NumericValueStatus
|
||||||
|
DifferenceAltitudeGNSSBaro int16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AirborneVelocityGroundSpeedNormal) UnmarshalBytes(data []byte) error {
|
||||||
|
if err := msg.unmarshalBytes(data, 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Airspeed, msg.AirspeedStatus = fields.ParseAirspeedSupersonic(data)
|
||||||
|
msg.DirectionEastWest = fields.ParseDirectionEastWest(data)
|
||||||
|
msg.VelocityEW, msg.VelocityEWStatus = fields.ParseVelocityEastWestNormal(data)
|
||||||
|
msg.DirectionNorthSouth = fields.ParseDirectionNorthSouth(data)
|
||||||
|
msg.VelocityNS, msg.VelocityNSStatus = fields.ParseVelocityNorthSouthNormal(data)
|
||||||
|
msg.DifferenceAltitudeGNSSBaro, msg.DifferenceAltitudeGNSSBaroStatus = fields.ParseHeightDifferenceFromBaro(data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AirborneVelocityGroundSpeedSupersonic is a message at the format BDS 9,0
|
||||||
|
type AirborneVelocityGroundSpeedSupersonic struct {
|
||||||
|
AirborneVelocity
|
||||||
|
DirectionEastWest fields.DirectionEastWest
|
||||||
|
VelocityEWStatus fields.NumericValueStatus
|
||||||
|
VelocityEW uint16
|
||||||
|
DirectionNorthSouth fields.DirectionNorthSouth
|
||||||
|
VelocityNSStatus fields.NumericValueStatus
|
||||||
|
VelocityNS uint16
|
||||||
|
DifferenceAltitudeGNSSBaroStatus fields.NumericValueStatus
|
||||||
|
DifferenceAltitudeGNSSBaro int16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AirborneVelocityGroundSpeedSupersonic) UnmarshalBytes(data []byte) error {
|
||||||
|
if err := msg.unmarshalBytes(data, 2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Airspeed, msg.AirspeedStatus = fields.ParseAirspeedSupersonic(data)
|
||||||
|
msg.DirectionEastWest = fields.ParseDirectionEastWest(data)
|
||||||
|
msg.VelocityEW, msg.VelocityEWStatus = fields.ParseVelocityEastWestSupersonic(data)
|
||||||
|
msg.DirectionNorthSouth = fields.ParseDirectionNorthSouth(data)
|
||||||
|
msg.VelocityNS, msg.VelocityNSStatus = fields.ParseVelocityNorthSouthSupersonic(data)
|
||||||
|
msg.DifferenceAltitudeGNSSBaro, msg.DifferenceAltitudeGNSSBaroStatus = fields.ParseHeightDifferenceFromBaro(data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirborneVelocityAirSpeedNormal struct {
|
||||||
|
AirborneVelocity
|
||||||
|
HeightDifferenceFromBaroStatus fields.NumericValueStatus
|
||||||
|
HeightDifferenceFromBaro int16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AirborneVelocity) unmarshalBytes(data []byte, subType byte) error {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return ErrLength
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.FormatType = (data[0] & 0xF8) >> 3
|
||||||
|
if msg.FormatType != 19 {
|
||||||
|
return ErrFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.SubType = data[0] & 0x07
|
||||||
|
if msg.SubType != subType {
|
||||||
|
return ErrSubType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.IntentChange = fields.ParseIntentChange(data)
|
||||||
|
msg.IFRCapability = fields.ParseIFRCapability(data)
|
||||||
|
msg.NavigationUncertaintyCategory = fields.ParseeNavigationUncertaintyCategory(data)
|
||||||
|
msg.MagneticHeading, msg.MagneticHeadingStatus = fields.ParseMagneticHeading(data)
|
||||||
|
msg.VerticalRateSource = fields.ParseVerticalRateSource(data)
|
||||||
|
msg.VerticalRate, msg.VerticalRateStatus = fields.ParseVerticalRate(data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AirborneVelocityAirSpeedNormal) UnmarshalBytes(data []byte) error {
|
||||||
|
if err := msg.unmarshalBytes(data, 3); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Airspeed, msg.AirspeedStatus = fields.ParseAirspeedNormal(data)
|
||||||
|
msg.HeightDifferenceFromBaro, msg.HeightDifferenceFromBaroStatus = fields.ParseHeightDifferenceFromBaro(data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirborneVelocityAirSpeedSupersonic struct {
|
||||||
|
AirborneVelocity
|
||||||
|
HeightDifferenceFromBaroStatus fields.NumericValueStatus
|
||||||
|
HeightDifferenceFromBaro int16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AirborneVelocityAirSpeedSupersonic) UnmarshalBytes(data []byte) error {
|
||||||
|
if err := msg.unmarshalBytes(data, 4); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Airspeed, msg.AirspeedStatus = fields.ParseAirspeedSupersonic(data)
|
||||||
|
msg.HeightDifferenceFromBaro, msg.HeightDifferenceFromBaroStatus = fields.ParseHeightDifferenceFromBaro(data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AircraftStatusNoInformation struct {
|
||||||
|
FormatType byte
|
||||||
|
SubType byte
|
||||||
|
EmergencyPriorityStatus fields.EmergencyPriorityStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AircraftStatusNoInformation) UnmarshalBytes(data []byte) error {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return ErrLength
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.FormatType = (data[0] & 0xF8) >> 3
|
||||||
|
if msg.FormatType != 28 {
|
||||||
|
return ErrFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.SubType = data[0] & 0x07
|
||||||
|
if msg.SubType != 0 {
|
||||||
|
return ErrSubType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.EmergencyPriorityStatus = fields.ParseEmergencyPriorityStatus(data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AircraftStatusEmergency struct {
|
||||||
|
FormatType byte
|
||||||
|
SubType byte
|
||||||
|
EmergencyPriorityStatus fields.EmergencyPriorityStatus
|
||||||
|
ModeACode uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AircraftStatusEmergency) UnmarshalBytes(data []byte) error {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return ErrLength
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.FormatType = (data[0] & 0xF8) >> 3
|
||||||
|
if msg.FormatType != 28 {
|
||||||
|
return ErrFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.SubType = data[0] & 0x07
|
||||||
|
if msg.SubType != 1 {
|
||||||
|
return ErrSubType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.EmergencyPriorityStatus = fields.ParseEmergencyPriorityStatus(data)
|
||||||
|
msg.ModeACode = uint16((data[1]&0x1F)<<8) | uint16(data[2])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AircraftStatusACAS struct {
|
||||||
|
FormatType byte
|
||||||
|
SubType byte
|
||||||
|
ResolutionAdvisory ResolutionAdvisory
|
||||||
|
}
|
||||||
|
|
||||||
|
type TargetStateAndStatus0 struct {
|
||||||
|
FormatType byte
|
||||||
|
SubType byte
|
||||||
|
VerticalDataAvailableSourceIndicator fields.VerticalDataAvailableSourceIndicator
|
||||||
|
TargetAltitudeType fields.TargetAltitudeType
|
||||||
|
TargetAltitudeCapability fields.TargetAltitudeCapability
|
||||||
|
VerticalModeIndicator fields.VerticalModeIndicator
|
||||||
|
TargetAltitudeStatus fields.NumericValueStatus
|
||||||
|
TargetAltitude int32
|
||||||
|
HorizontalDataAvailableSourceIndicator fields.HorizontalDataAvailableSourceIndicator
|
||||||
|
TargetHeadingTrackAngleStatus fields.NumericValueStatus
|
||||||
|
TargetHeadingTrackAngle uint16
|
||||||
|
TargetHeadingTrackIndicator fields.TargetHeadingTrackIndicator
|
||||||
|
HorizontalModeIndicator fields.HorizontalModeIndicator
|
||||||
|
NavigationalAccuracyCategoryPosition fields.NavigationalAccuracyCategoryPositionV1
|
||||||
|
NICBaro fields.NICBaro
|
||||||
|
SurveillanceIntegrityLevel fields.SurveillanceIntegrityLevel
|
||||||
|
CapabilityModeCode fields.CapabilityModeCode
|
||||||
|
EmergencyPriorityStatus fields.EmergencyPriorityStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *TargetStateAndStatus0) UnmarshalBytes(data []byte) error {
|
||||||
|
if len(data) != 7 {
|
||||||
|
return ErrLength
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.FormatType = (data[0] & 0xF8) >> 3
|
||||||
|
if msg.FormatType != 29 {
|
||||||
|
return ErrFormatType
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.SubType = (data[0] & 0x06) >> 1
|
||||||
|
if msg.SubType != 0 {
|
||||||
|
return ErrSubType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var identificationCharacterCoding = []byte{
|
||||||
|
'#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
||||||
|
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#', '#', '#', '#', '#',
|
||||||
|
' ', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#',
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '#', '#', '#', '#', '#',
|
||||||
|
}
|
||||||
137
protocol/adsb/ra.go
Normal file
137
protocol/adsb/ra.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package adsb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolutionAdvisory is an ACAS message providing information about ResolutionAdvisory
|
||||||
|
//
|
||||||
|
// Defined at 3.1.2.8.3.1 and 4.3.8.4.2.4
|
||||||
|
type ResolutionAdvisory struct {
|
||||||
|
ActiveRA ActiveResolutionAdvisory
|
||||||
|
RAComplement RAComplement
|
||||||
|
RATerminatedIndicator RATerminatedIndicator
|
||||||
|
MultipleThreatEncounter MultipleThreatEncounter
|
||||||
|
ThreatTypeIndicator ThreatTypeIndicator
|
||||||
|
ThreatIdentityAddress *ThreatIdentityAddress
|
||||||
|
ThreatIdentityAltitude *ThreatIdentityAltitude
|
||||||
|
ThreatIdentityRange *ThreatIdentityRange
|
||||||
|
ThreatIdentityBearing *ThreatIdentityBearing
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThreatTypeIndicator indicates the type of information contained in the TID part of the message
|
||||||
|
type ThreatTypeIndicator int
|
||||||
|
|
||||||
|
// ThreatIdentityAddress is a 3 bytes ICAO Address. The Most Significant Byte of the address
|
||||||
|
// is always 0.
|
||||||
|
type ThreatIdentityAddress uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ThreatTypeNoIdentity ThreatTypeIndicator = iota // No identity data in TID
|
||||||
|
ThreatTypeModeS // Contains a Mode S transponder address
|
||||||
|
ThreatTypeAltitudeRangeBearing // Contains altitude, range and bearing data
|
||||||
|
ThreatTypeReserved3 // Reserved
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThreatIdentityAltitude is the altitude of the threat. It is given in 100 feet increment
|
||||||
|
type ThreatIdentityAltitude struct {
|
||||||
|
AltitudeValid bool
|
||||||
|
AltitudeInFeet int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseThreatTypeIndicator(data []byte) ThreatTypeIndicator {
|
||||||
|
return ThreatTypeIndicator((data[2] & 0x0C) >> 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseThreatIdentityAltitude reads the altitude code from a message
|
||||||
|
func parseThreatIdentityAltitude(data []byte) (ThreatIdentityAltitude, error) {
|
||||||
|
|
||||||
|
// Altitude code is a 13 bits fields, so read a uint16
|
||||||
|
// byte data[2] | data[3] | data[4]
|
||||||
|
// bit 19 20 21 22 23|24 25 26 27 28 29 30 31|32 33 34 35 36
|
||||||
|
// value _ _ _ C1 A1|C2 A2 C4 A4 0 B1 D1 B2|D2 B4 D4 _ _
|
||||||
|
|
||||||
|
// Start by D2 B4 D4
|
||||||
|
altitudeCode := uint16(data[4]&0xE0) >> 5
|
||||||
|
// Then pack B1 D1 B2
|
||||||
|
altitudeCode += uint16(data[3]&0x07) << 3
|
||||||
|
// Then C2 A2 C4 A4
|
||||||
|
altitudeCode += uint16(data[3]&0xF0) << 2
|
||||||
|
// Then C1 A1
|
||||||
|
altitudeCode += uint16(data[2]&0x03) << 2
|
||||||
|
|
||||||
|
// Detect invalid altitude
|
||||||
|
if altitudeCode == 0 {
|
||||||
|
return ThreatIdentityAltitude{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c1 := (altitudeCode & 0x0800) != 0
|
||||||
|
a1 := (altitudeCode & 0x0400) != 0
|
||||||
|
c2 := (altitudeCode & 0x0200) != 0
|
||||||
|
a2 := (altitudeCode & 0x0100) != 0
|
||||||
|
c4 := (altitudeCode & 0x0080) != 0
|
||||||
|
a4 := (altitudeCode & 0x0040) != 0
|
||||||
|
b1 := (altitudeCode & 0x0020) != 0
|
||||||
|
d1 := (altitudeCode & 0x0010) != 0
|
||||||
|
b2 := (altitudeCode & 0x0008) != 0
|
||||||
|
d2 := (altitudeCode & 0x0004) != 0
|
||||||
|
b4 := (altitudeCode & 0x0002) != 0
|
||||||
|
d4 := (altitudeCode & 0x0001) != 0
|
||||||
|
|
||||||
|
altitudeFeet, err := gillhamToAltitude(d1, d2, d4, a1, a2, a4, b1, b2, b4, c1, c2, c4)
|
||||||
|
if err != nil {
|
||||||
|
return ThreatIdentityAltitude{}, errors.New("adsb: the altitude field is malformed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ThreatIdentityAltitude{true, altitudeFeet}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThreatIdentityRange is TIDR (threat identity data range subfield). This 7-bit subfield (76-82) shall contain the
|
||||||
|
// most recent threat range estimated by ACAS.
|
||||||
|
type ThreatIdentityRange byte
|
||||||
|
|
||||||
|
func parseThreatIdentityRange(data []byte) ThreatIdentityRange {
|
||||||
|
return ThreatIdentityRange((data[4]&0x1F)<<2 + (data[5]&0xA0)>>6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThreatIdentityBearing is TIDR (threat identity data bearing subfield). This 6-bit subfield (83-88) shall contain the
|
||||||
|
// most recent estimated bearing of the threat aircraft, relative to the ACAS aircraft heading.
|
||||||
|
type ThreatIdentityBearing byte
|
||||||
|
|
||||||
|
func parseThreatIdentityBearing(data []byte) ThreatIdentityBearing {
|
||||||
|
return ThreatIdentityBearing(data[5] & 0x3F)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResolutionAdvisory(data []byte) (*ResolutionAdvisory, error) {
|
||||||
|
if len(data) != 6 {
|
||||||
|
return nil, errors.New("adsb: data for ACAS ResolutionAdvisory must be 6 bytes long")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format of the message is as follows:
|
||||||
|
// 0 1 2 3 4 5
|
||||||
|
// | RAC | R R M T TID | TID | TID | TID |
|
||||||
|
// ARA | ARA RAC | A A T T d d | d d d d d d d d | d d d d d d d d | d d d d d d _ _ |
|
||||||
|
// | RAC | C T E I TIDA| TIDA | TIDA TIDR |TIDR TIDB |
|
||||||
|
// a a a a a a a a | a a a a a a c c | c c t m i i a a | a a a a a a a a | a a a r r r r r | r r b b b b b b |
|
||||||
|
|
||||||
|
ra := new(ResolutionAdvisory)
|
||||||
|
switch ra.ThreatTypeIndicator = parseThreatTypeIndicator(data); ra.ThreatTypeIndicator {
|
||||||
|
case ThreatTypeModeS:
|
||||||
|
addr := ThreatIdentityAddress((binary.BigEndian.Uint32(data[2:]) >> 2) & 0xFFFFFF)
|
||||||
|
ra.ThreatIdentityAddress = &addr
|
||||||
|
|
||||||
|
case ThreatTypeAltitudeRangeBearing:
|
||||||
|
altitude, err := parseThreatIdentityAltitude(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ra.ThreatIdentityAltitude = &altitude
|
||||||
|
|
||||||
|
threatRange := parseThreatIdentityRange(data)
|
||||||
|
ra.ThreatIdentityRange = &threatRange
|
||||||
|
|
||||||
|
bearing := parseThreatIdentityBearing(data)
|
||||||
|
ra.ThreatIdentityBearing = &bearing
|
||||||
|
}
|
||||||
|
}
|
||||||
335
protocol/aprs/_attic/data_mice.go
Normal file
335
protocol/aprs/_attic/data_mice.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
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",
|
||||||
|
}
|
||||||
219
protocol/aprs/_attic/position.go
Normal file
219
protocol/aprs/_attic/position.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package aprs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrLatitude = errors.New("aprs: invalid latitude")
|
||||||
|
ErrLongitude = errors.New("aprs: invalid longitude")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Latitude is the north-south position. Positive values are North, negative are South.
|
||||||
|
type Latitude float64
|
||||||
|
|
||||||
|
func LatitudeFromDMH(degrees, minutes, hundreths int, north bool) Latitude {
|
||||||
|
v := float64(degrees) + float64(minutes)/60 + float64(hundreths)/6000
|
||||||
|
|
||||||
|
for v > 90 {
|
||||||
|
v -= 180
|
||||||
|
}
|
||||||
|
for v < -90 {
|
||||||
|
v += 180
|
||||||
|
}
|
||||||
|
|
||||||
|
if north {
|
||||||
|
return Latitude(v)
|
||||||
|
}
|
||||||
|
return -Latitude(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lat Latitude) DMH() (degrees, minutes, hundreths int, north bool) {
|
||||||
|
degrees = int(lat)
|
||||||
|
minutes = int((float64(lat) - float64(degrees)) * 60)
|
||||||
|
hundreths = int((float64(lat) - float64(degrees) - float64(minutes)/60) * 6000)
|
||||||
|
|
||||||
|
if hundreths == 100 {
|
||||||
|
hundreths = 0
|
||||||
|
minutes += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if minutes == 60 {
|
||||||
|
minutes = 0
|
||||||
|
degrees += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
north = lat >= 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lat *Latitude) ParseCompressed(b []byte) error {
|
||||||
|
if len(b) != 4 {
|
||||||
|
return ErrLatitude
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := base91Decode(string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*lat = Latitude(90 - float64(n)/380926)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lat *Latitude) ParseUncompressed(b []byte) error {
|
||||||
|
if len(b) != 8 || b[4] != '.' {
|
||||||
|
return ErrLatitude
|
||||||
|
}
|
||||||
|
|
||||||
|
var north bool
|
||||||
|
switch b[7] {
|
||||||
|
case 'N':
|
||||||
|
north = true
|
||||||
|
case 'S':
|
||||||
|
north = false
|
||||||
|
default:
|
||||||
|
return ErrLatitude
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
degrees, minutes, hundreths int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if degrees, err = parseBytesWithSpaces(b[0:2]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if minutes, err = parseBytesWithSpaces(b[2:4]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hundreths, err = parseBytesWithSpaces(b[5:7]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*lat = LatitudeFromDMH(degrees, minutes, hundreths, north)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lat Latitude) Compressed(b []byte) {
|
||||||
|
v := int((90 - float64(lat)) * 380926.0)
|
||||||
|
base91Encode(b, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lat Latitude) Uncompressed(b []byte) {
|
||||||
|
var (
|
||||||
|
degrees, minutes, hundreths, north = lat.DMH()
|
||||||
|
v = fmt.Sprintf("%02d%02d.%02d", degrees, minutes, hundreths)
|
||||||
|
)
|
||||||
|
if north {
|
||||||
|
v += "N"
|
||||||
|
} else {
|
||||||
|
v += "S"
|
||||||
|
}
|
||||||
|
copy(b, []byte(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Longitude is the east-west position. Positive values are East, negative are West.
|
||||||
|
type Longitude float64
|
||||||
|
|
||||||
|
func LongitudeFromDMH(degrees, minutes, hundreths int, east bool) Longitude {
|
||||||
|
v := float64(degrees) + float64(minutes)/60 + float64(hundreths)/6000
|
||||||
|
|
||||||
|
for v > 180 {
|
||||||
|
v -= 360
|
||||||
|
}
|
||||||
|
for v < -180 {
|
||||||
|
v += 360
|
||||||
|
}
|
||||||
|
|
||||||
|
if east {
|
||||||
|
return Longitude(v)
|
||||||
|
}
|
||||||
|
return -Longitude(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (long Longitude) DMH() (degrees, minutes, hundreths int, east bool) {
|
||||||
|
degrees = int(long)
|
||||||
|
minutes = int((float64(long) - float64(degrees)) * 60)
|
||||||
|
hundreths = int((float64(long) - float64(degrees) - float64(minutes)/60) * 6000)
|
||||||
|
|
||||||
|
if hundreths == 100 {
|
||||||
|
hundreths = 0
|
||||||
|
minutes += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if minutes == 60 {
|
||||||
|
minutes = 0
|
||||||
|
degrees += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
east = long >= 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (long *Longitude) ParseCompressed(b []byte) error {
|
||||||
|
if len(b) != 4 {
|
||||||
|
return ErrLatitude
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := base91Decode(string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*long = Longitude(float64(n)/190463.0 - 180)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (long *Longitude) ParseUncompressed(b []byte) error {
|
||||||
|
if len(b) != 9 || b[5] != '.' {
|
||||||
|
return ErrLongitude
|
||||||
|
}
|
||||||
|
|
||||||
|
var east bool
|
||||||
|
switch b[8] {
|
||||||
|
case 'E':
|
||||||
|
east = true
|
||||||
|
case 'W':
|
||||||
|
east = false
|
||||||
|
default:
|
||||||
|
return ErrLongitude
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
degrees, minutes, hundreths int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if degrees, err = parseBytesWithSpaces(b[0:3]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if minutes, err = parseBytesWithSpaces(b[3:5]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hundreths, err = parseBytesWithSpaces(b[6:8]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*long = LongitudeFromDMH(degrees, minutes, hundreths, east)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (long Longitude) Compressed(b []byte) {
|
||||||
|
v := int((180 + float64(long)) * 190463)
|
||||||
|
base91Encode(b, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (long Longitude) Uncompressed(b []byte) {
|
||||||
|
var (
|
||||||
|
degrees, minutes, hundreths, east = long.DMH()
|
||||||
|
v = fmt.Sprintf("%03d%02d.%02d", degrees, minutes, hundreths)
|
||||||
|
)
|
||||||
|
if east {
|
||||||
|
v += "E"
|
||||||
|
} else {
|
||||||
|
v += "W"
|
||||||
|
}
|
||||||
|
copy(b, []byte(b))
|
||||||
|
}
|
||||||
53
protocol/aprs/_attic/position_test.go
Normal file
53
protocol/aprs/_attic/position_test.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package aprs
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLatitude(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Test string
|
||||||
|
Want Latitude
|
||||||
|
}{
|
||||||
|
{"4903.50N", 49.05833333333333},
|
||||||
|
{"4903.50S", -49.05833333333333},
|
||||||
|
{"4903.5 S", -49.05833333333333},
|
||||||
|
{"4903. S", -49.05},
|
||||||
|
{"490 . S", -49},
|
||||||
|
{"4 . S", -40},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.Test, func(t *testing.T) {
|
||||||
|
var lat Latitude
|
||||||
|
if err := lat.ParseUncompressed([]byte(test.Test)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !testAlmostEqual(float64(test.Want), float64(lat)) {
|
||||||
|
t.Errorf("expected %f, got %f", test.Want, lat)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLongitude(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Test string
|
||||||
|
Want Longitude
|
||||||
|
}{
|
||||||
|
{"00000.00E", 0},
|
||||||
|
{"00000.00W", 0},
|
||||||
|
{"00000.98W", -0.016333},
|
||||||
|
{"00098. W", -1.633333},
|
||||||
|
{"098 . W", -98.000000},
|
||||||
|
{"9 . W", -180.000000},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.Test, func(t *testing.T) {
|
||||||
|
var long Longitude
|
||||||
|
if err := long.ParseUncompressed([]byte(test.Test)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !testAlmostEqual(float64(test.Want), float64(long)) {
|
||||||
|
t.Errorf("expected %f, got %f", test.Want, long)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -217,6 +217,7 @@ func (client *ProxyClient) copy(dst, src net.Conn, host, dir string, call *strin
|
|||||||
func (client *ProxyClient) Info() *radio.Info {
|
func (client *ProxyClient) Info() *radio.Info {
|
||||||
// We have very little information actually, but here we go:
|
// We have very little information actually, but here we go:
|
||||||
return &radio.Info{
|
return &radio.Info{
|
||||||
|
ID: client.myCall,
|
||||||
Name: client.myCall,
|
Name: client.myCall,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ func NewCompanion(conn io.ReadWriteCloser) (*Node, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepeater(conn io.ReadWriteCloser, hasSNR bool) (*Node, error) {
|
func NewRepeater(conn io.ReadWriteCloser) (*Node, error) {
|
||||||
driver := newRepeaterDriver(conn, hasSNR)
|
driver := newRepeaterDriver(conn)
|
||||||
|
|
||||||
if err := driver.Setup(); err != nil {
|
if err := driver.Setup(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -60,7 +60,7 @@ func (dev *Node) Info() *radio.Info {
|
|||||||
return dev.driver.Info()
|
return dev.driver.Info()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dev *Node) Stats() map[string]any {
|
func (dev *Node) Stats() <-chan map[string]any {
|
||||||
return dev.driver.Stats()
|
return dev.driver.Stats()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ type nodeDriver interface {
|
|||||||
Setup() error
|
Setup() error
|
||||||
|
|
||||||
Packets() <-chan *Packet
|
Packets() <-chan *Packet
|
||||||
Stats() map[string]any
|
Stats() <-chan map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type nodeTracer interface {
|
type nodeTracer interface {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type companionDriver struct {
|
|||||||
info companionInfo
|
info companionInfo
|
||||||
traceTag uint32
|
traceTag uint32
|
||||||
traceAuthCode uint32
|
traceAuthCode uint32
|
||||||
stats map[string]any
|
stats chan map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type companionDriverWaiting struct {
|
type companionDriverWaiting struct {
|
||||||
@@ -131,6 +131,7 @@ func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver {
|
|||||||
conn: conn,
|
conn: conn,
|
||||||
waiting: make(chan *companionDriverWaiting, 16),
|
waiting: make(chan *companionDriverWaiting, 16),
|
||||||
traceTag: rand.Uint32(),
|
traceTag: rand.Uint32(),
|
||||||
|
stats: make(chan map[string]any, 2),
|
||||||
//traceAuthCode: rand.Uint32(),
|
//traceAuthCode: rand.Uint32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,7 @@ func (drv *companionDriver) Setup() (err error) {
|
|||||||
if err = drv.sendAppStart(); err != nil {
|
if err = drv.sendAppStart(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = drv.sendDeviceInfo(); err != nil {
|
if err = drv.getDeviceInfo(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -209,7 +210,7 @@ func (drv *companionDriver) Info() *radio.Info {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (drv *companionDriver) Stats() map[string]any {
|
func (drv *companionDriver) Stats() <-chan map[string]any {
|
||||||
return drv.stats
|
return drv.stats
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +398,7 @@ func (drv *companionDriver) sendAppStart() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (drv *companionDriver) sendDeviceInfo() (err error) {
|
func (drv *companionDriver) getDeviceInfo() (err error) {
|
||||||
var (
|
var (
|
||||||
args = []byte{0x03}
|
args = []byte{0x03}
|
||||||
data []byte
|
data []byte
|
||||||
@@ -425,6 +426,11 @@ func (drv *companionDriver) sendDeviceInfo() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (drv *companionDriver) getPublicKey() (err error) {
|
||||||
|
// TODO
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (drv *companionDriver) poll() {
|
func (drv *companionDriver) poll() {
|
||||||
for {
|
for {
|
||||||
frame, err := drv.readFrame()
|
frame, err := drv.readFrame()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package meshcore
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
@@ -23,7 +24,7 @@ type repeaterDriver struct {
|
|||||||
lastFrame []byte
|
lastFrame []byte
|
||||||
lastFrameAt time.Time
|
lastFrameAt time.Time
|
||||||
info repeaterInfo
|
info repeaterInfo
|
||||||
stats map[string]any
|
stats chan map[string]any
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ type repeaterInfo struct {
|
|||||||
Type NodeType
|
Type NodeType
|
||||||
Power byte // in dBm
|
Power byte // in dBm
|
||||||
MaxPower byte // in dBm
|
MaxPower byte // in dBm
|
||||||
PublicKey [32]byte
|
PublicKey string
|
||||||
Latitude float64
|
Latitude float64
|
||||||
Longitude float64
|
Longitude float64
|
||||||
HasMultiACKs bool
|
HasMultiACKs bool
|
||||||
@@ -98,15 +99,20 @@ type repeaterInfo struct {
|
|||||||
Manufacturer string
|
Manufacturer string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRepeaterDriver(conn io.ReadWriteCloser, hasSNR bool) *repeaterDriver {
|
func newRepeaterDriver(conn io.ReadWriteCloser) *repeaterDriver {
|
||||||
return &repeaterDriver{
|
return &repeaterDriver{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
waiting: make(chan *repeaterDriverWaiting, 16),
|
waiting: make(chan *repeaterDriverWaiting, 16),
|
||||||
hasSNR: hasSNR,
|
stats: make(chan map[string]any, 2),
|
||||||
stats: make(map[string]any),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRepeaterDriverWithSNRPatch(conn io.ReadWriteCloser) *repeaterDriver {
|
||||||
|
driver := newRepeaterDriver(conn)
|
||||||
|
driver.hasSNR = true
|
||||||
|
return driver
|
||||||
|
}
|
||||||
|
|
||||||
func (drv *repeaterDriver) Close() error {
|
func (drv *repeaterDriver) Close() error {
|
||||||
return drv.conn.Close()
|
return drv.conn.Close()
|
||||||
}
|
}
|
||||||
@@ -150,6 +156,7 @@ func (drv *repeaterDriver) Info() *radio.Info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &radio.Info{
|
return &radio.Info{
|
||||||
|
ID: drv.info.PublicKey,
|
||||||
Name: drv.info.Name,
|
Name: drv.info.Name,
|
||||||
Manufacturer: manufacturer,
|
Manufacturer: manufacturer,
|
||||||
Device: device,
|
Device: device,
|
||||||
@@ -179,7 +186,7 @@ func (drv *repeaterDriver) RawPackets() <-chan *protocol.Packet {
|
|||||||
return drv.rawPackets
|
return drv.rawPackets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (drv *repeaterDriver) Stats() map[string]any {
|
func (drv *repeaterDriver) Stats() <-chan map[string]any {
|
||||||
return drv.stats
|
return drv.stats
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +200,11 @@ func (drv *repeaterDriver) queryDeviceInfo() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch public key
|
||||||
|
if drv.info.PublicKey, err = drv.writeCommand("get", "public.key"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var line string
|
var line string
|
||||||
|
|
||||||
// Fetch frequency, bandwidth and LoRa settings
|
// Fetch frequency, bandwidth and LoRa settings
|
||||||
@@ -412,24 +424,26 @@ func (drv *repeaterDriver) poll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (drv *repeaterDriver) pollStats() {
|
func (drv *repeaterDriver) pollStats() {
|
||||||
ticker := time.NewTicker(time.Minute)
|
ticker := time.NewTicker(time.Second * 10)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
stats := make(map[string]any)
|
||||||
|
|
||||||
neighbors, err := drv.getNeighbors()
|
neighbors, err := drv.getNeighbors()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Warnf("meshcore: failed to get neighbors: %v", err)
|
Logger.Warnf("meshcore: failed to get neighbors: %v", err)
|
||||||
} else {
|
} else {
|
||||||
drv.stats["neighbors"] = neighbors
|
stats["neighbors"] = neighbors
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := drv.writeCommand("stats")
|
response, err := drv.writeCommand("stats-core")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Warnf("meshcore: failed to get stats: %v", err)
|
Logger.Warnf("meshcore: failed to get stats: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := make(map[string]any)
|
neighborStats := make(map[string]any)
|
||||||
for _, line := range strings.Split(response, "\n") {
|
for _, line := range strings.Split(response, "\n") {
|
||||||
parts := strings.SplitN(line, "=", 2)
|
parts := strings.SplitN(line, "=", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
@@ -439,15 +453,53 @@ func (drv *repeaterDriver) pollStats() {
|
|||||||
value := parts[1]
|
value := parts[1]
|
||||||
|
|
||||||
if i, err := strconv.Atoi(value); err == nil {
|
if i, err := strconv.Atoi(value); err == nil {
|
||||||
stats[key] = i
|
neighborStats[key] = i
|
||||||
} else if f, err := strconv.ParseFloat(value, 64); err == nil {
|
} else if f, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
stats[key] = f
|
neighborStats[key] = f
|
||||||
} else {
|
} else {
|
||||||
stats[key] = value
|
neighborStats[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drv.stats = stats
|
var (
|
||||||
|
coreStats = make(map[string]any)
|
||||||
|
radioStats = make(map[string]any)
|
||||||
|
packetStats = make(map[string]any)
|
||||||
|
)
|
||||||
|
if response, err := drv.writeCommand("stats-core"); err == nil {
|
||||||
|
if err = json.Unmarshal([]byte(response), &coreStats); err != nil {
|
||||||
|
Logger.Warnf("meshcore: failed to decode core stats: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.Warnf("meshcore: failed to get core stats: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response, err := drv.writeCommand("stats-radio"); err == nil {
|
||||||
|
if err = json.Unmarshal([]byte(response), &radioStats); err != nil {
|
||||||
|
Logger.Warnf("meshcore: failed to decode radio stats: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.Warnf("meshcore: failed to get radio stats: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response, err := drv.writeCommand("stats-packets"); err == nil {
|
||||||
|
if err = json.Unmarshal([]byte(response), &packetStats); err != nil {
|
||||||
|
Logger.Warnf("meshcore: failed to decode packet stats: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.Warnf("meshcore: failed to get packet stats: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["neighbors"] = neighborStats
|
||||||
|
stats["core"] = coreStats
|
||||||
|
stats["radio"] = radioStats
|
||||||
|
stats["packets"] = packetStats
|
||||||
|
|
||||||
|
select {
|
||||||
|
case drv.stats <- stats:
|
||||||
|
default:
|
||||||
|
Logger.Warn("meshcore: stats channel full, dropping stats")
|
||||||
|
}
|
||||||
|
|
||||||
<-ticker.C
|
<-ticker.C
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user