Fixed code smells
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package aprs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func base91Decode(s string) (n int, err error) {
|
func base91Decode(s string) (n int, err error) {
|
||||||
@@ -18,6 +17,7 @@ func base91Decode(s string) (n int, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func base91Encode(n int) string {
|
func base91Encode(n int) string {
|
||||||
var s []string
|
var s []string
|
||||||
for n > 0 {
|
for n > 0 {
|
||||||
@@ -27,3 +27,4 @@ func base91Encode(n int) string {
|
|||||||
}
|
}
|
||||||
return strings.Join(s, "")
|
return strings.Join(s, "")
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ func (p *Packet) parse() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.Position = &pos
|
p.Position = &pos
|
||||||
p.parseMicEData()
|
_ = p.parseMicEData()
|
||||||
|
|
||||||
return nil // there is no additional data to parse
|
return nil // there is no additional data to parse
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (priv *PrivateKey) Sign(_ io.Reader, digest []byte, _ crypto.SignerOpts) (s
|
|||||||
|
|
||||||
func (priv *PrivateKey) Public() crypto.PublicKey {
|
func (priv *PrivateKey) Public() crypto.PublicKey {
|
||||||
p := &PublicKey{}
|
p := &PublicKey{}
|
||||||
newPublicKey(p, priv.pub[:])
|
_, _ = newPublicKey(p, priv.pub[:])
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +118,9 @@ func GenerateKey() (*PublicKey, *PrivateKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateKey(priv *PrivateKey) (*PrivateKey, error) {
|
func generateKey(priv *PrivateKey) (*PrivateKey, error) {
|
||||||
rand.Read(priv.seed[:])
|
if _, err := rand.Read(priv.seed[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
precomputePrivateKey(priv)
|
precomputePrivateKey(priv)
|
||||||
return priv, nil
|
return priv, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,15 +357,6 @@ func (drv *companionDriver) wait(expect ...byte) ([]byte, error) {
|
|||||||
return wait.Wait()
|
return wait.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func bytesContains(b byte, slice []byte) bool {
|
|
||||||
for _, v := range slice {
|
|
||||||
if v == b {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (drv *companionDriver) handlePushFrame(b []byte) {
|
func (drv *companionDriver) handlePushFrame(b []byte) {
|
||||||
if len(b) < 1 {
|
if len(b) < 1 {
|
||||||
return // illegal
|
return // illegal
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ import (
|
|||||||
"git.maze.io/go/ham/protocol/meshcore/crypto"
|
"git.maze.io/go/ham/protocol/meshcore/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var zeroPositionBytes = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||||
zeroTime time.Time
|
|
||||||
zeroPositionBytes = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
|
||||||
)
|
|
||||||
|
|
||||||
type Payload interface {
|
type Payload interface {
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ func decodeTime(b []byte) time.Time {
|
|||||||
return time.Unix(int64(binary.LittleEndian.Uint32(b)), 0).UTC()
|
return time.Unix(int64(binary.LittleEndian.Uint32(b)), 0).UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeFrequency(b []byte, f float64) {
|
|
||||||
binary.LittleEndian.PutUint32(b, uint32(f*1e3))
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeFrequency(b []byte) float64 {
|
func decodeFrequency(b []byte) float64 {
|
||||||
return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3
|
return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3
|
||||||
}
|
}
|
||||||
|
|||||||
173
protocol/meshtastic/node.go
Normal file
173
protocol/meshtastic/node.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package meshtastic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodeID uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BroadcastNodeID is the special NodeID used when broadcasting a packet to a channel.
|
||||||
|
BroadcastNodeID NodeID = math.MaxUint32
|
||||||
|
|
||||||
|
// BroadcastNodeIDNoLora is a special broadcast address that excludes LoRa transmission.
|
||||||
|
// Used for MQTT-only broadcasts. This is ^all with the NO_LORA flag (0x40) cleared.
|
||||||
|
BroadcastNodeIDNoLora NodeID = math.MaxUint32 ^ 0x40
|
||||||
|
|
||||||
|
// ReservedNodeIDThreshold is the threshold at which NodeIDs are considered reserved. Random NodeIDs should not
|
||||||
|
// be generated below this threshold.
|
||||||
|
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L461
|
||||||
|
reservedNodeIDThreshold NodeID = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseNodeID parses a NodeID from various string formats:
|
||||||
|
// - "!abcd1234" (Meshtastic format with ! prefix)
|
||||||
|
// - "0xabcd1234" (hex with 0x prefix)
|
||||||
|
// - "abcd1234" (plain hex)
|
||||||
|
// - "12345678" (decimal)
|
||||||
|
func ParseNodeID(s string) (NodeID, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return 0, fmt.Errorf("empty node ID string")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle !prefix format
|
||||||
|
if strings.HasPrefix(s, "!") {
|
||||||
|
s = s[1:]
|
||||||
|
n, err := strconv.ParseUint(s, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return NodeID(n), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 0x prefix
|
||||||
|
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
||||||
|
n, err := strconv.ParseUint(s[2:], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return NodeID(n), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try hex first if it looks like hex (contains a-f)
|
||||||
|
sLower := strings.ToLower(s)
|
||||||
|
if strings.ContainsAny(sLower, "abcdef") {
|
||||||
|
n, err := strconv.ParseUint(s, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return NodeID(n), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try decimal
|
||||||
|
n, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
// Fall back to hex for 8-char strings
|
||||||
|
if len(s) == 8 {
|
||||||
|
n, err = strconv.ParseUint(s, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return NodeID(n), nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return NodeID(n), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint32 returns the underlying uint32 value of the NodeID.
|
||||||
|
func (n NodeID) Uint32() uint32 {
|
||||||
|
return uint32(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String converts the NodeID to a hex formatted string.
|
||||||
|
// This is typically how NodeIDs are displayed in Meshtastic UIs.
|
||||||
|
func (n NodeID) String() string {
|
||||||
|
return fmt.Sprintf("!%08x", uint32(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes converts the NodeID to a byte slice
|
||||||
|
func (n NodeID) Bytes() []byte {
|
||||||
|
bytes := make([]byte, 4) // uint32 is 4 bytes
|
||||||
|
binary.BigEndian.PutUint32(bytes, n.Uint32())
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultLongName returns the default long node name based on the NodeID.
|
||||||
|
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L382
|
||||||
|
func (n NodeID) DefaultLongName() string {
|
||||||
|
bytes := make([]byte, 4) // uint32 is 4 bytes
|
||||||
|
binary.BigEndian.PutUint32(bytes, n.Uint32())
|
||||||
|
return fmt.Sprintf("Meshtastic %04x", bytes[2:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultShortName returns the default short node name based on the NodeID.
|
||||||
|
// Last two bytes of the NodeID represented in hex.
|
||||||
|
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L382
|
||||||
|
func (n NodeID) DefaultShortName() string {
|
||||||
|
bytes := make([]byte, 4) // uint32 is 4 bytes
|
||||||
|
binary.BigEndian.PutUint32(bytes, n.Uint32())
|
||||||
|
return fmt.Sprintf("%04x", bytes[2:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler for use with config parsers like Viper.
|
||||||
|
func (n *NodeID) UnmarshalText(text []byte) error {
|
||||||
|
parsed, err := ParseNodeID(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*n = parsed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler.
|
||||||
|
func (n NodeID) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(n.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReservedID returns true if this is a reserved or broadcast NodeID.
|
||||||
|
func (n NodeID) IsReservedID() bool {
|
||||||
|
return n < reservedNodeIDThreshold || n >= BroadcastNodeIDNoLora
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBroadcast returns true if this is any form of broadcast address.
|
||||||
|
func (n NodeID) IsBroadcast() bool {
|
||||||
|
return n == BroadcastNodeID || n == BroadcastNodeIDNoLora
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMacAddress returns a MAC address string derived from the NodeID.
|
||||||
|
// This creates a locally administered unicast MAC address.
|
||||||
|
func (n NodeID) ToMacAddress() string {
|
||||||
|
bytes := n.Bytes()
|
||||||
|
// Use 0x02 as the first octet (locally administered, unicast)
|
||||||
|
// Then 0x00 as padding, followed by the 4 bytes of the NodeID
|
||||||
|
return fmt.Sprintf("02:00:%02x:%02x:%02x:%02x", bytes[0], bytes[1], bytes[2], bytes[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomNodeID returns a randomised NodeID.
|
||||||
|
// It's recommended to call this the first time a node is started and persist the result.
|
||||||
|
//
|
||||||
|
// Hardware meshtastic nodes first try a NodeID of the last four bytes of the BLE MAC address. If that ID is already in
|
||||||
|
// use or invalid, a random NodeID is generated.
|
||||||
|
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L466
|
||||||
|
func RandomNodeID() (NodeID, error) {
|
||||||
|
// Generates a random uint32 between reservedNodeIDThreshold and math.MaxUint32
|
||||||
|
randomInt, err := rand.Int(
|
||||||
|
rand.Reader,
|
||||||
|
big.NewInt(
|
||||||
|
int64(math.MaxUint32-reservedNodeIDThreshold.Uint32()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return NodeID(0), fmt.Errorf("reading entropy: %w", err)
|
||||||
|
}
|
||||||
|
r := uint32(randomInt.Uint64()) + reservedNodeIDThreshold.Uint32()
|
||||||
|
return NodeID(r), nil
|
||||||
|
}
|
||||||
54
protocol/meshtastic/packet.go
Normal file
54
protocol/meshtastic/packet.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package meshtastic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minPacketSize = 4 + 4 + 4 + 1 + 1 + 1 + 1
|
||||||
|
maxPayloadSize = 237
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidPacket signals the source buffer does not contain a valid packet.
|
||||||
|
ErrInvalidPacket = errors.New("meshtastic: invalid packet")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Packet struct {
|
||||||
|
Destination NodeID
|
||||||
|
Source NodeID
|
||||||
|
ID uint32
|
||||||
|
Flags uint8
|
||||||
|
ChannelHash uint8
|
||||||
|
NextHop uint8
|
||||||
|
RelayNode uint8
|
||||||
|
PayloadLength int
|
||||||
|
Payload [maxPayloadSize]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (packet *Packet) Decode(data []byte) error {
|
||||||
|
if len(data) < minPacketSize {
|
||||||
|
return ErrInvalidPacket
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.Destination = parseNodeID(data[0:])
|
||||||
|
packet.Source = parseNodeID(data[4:])
|
||||||
|
packet.ID = binary.LittleEndian.Uint32(data[8:])
|
||||||
|
packet.Flags = data[12]
|
||||||
|
packet.ChannelHash = data[13]
|
||||||
|
packet.NextHop = data[14]
|
||||||
|
packet.RelayNode = data[15]
|
||||||
|
packet.PayloadLength = len(data[16:])
|
||||||
|
copy(packet.Payload[:], data[16:])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (packet *Packet) HopLimit() int {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNodeID(data []byte) NodeID {
|
||||||
|
return NodeID(binary.LittleEndian.Uint32(data))
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ var parseLocatorMult = []struct {
|
|||||||
{upper[:18], lower[:18], 20.0},
|
{upper[:18], lower[:18], 20.0},
|
||||||
{upper[:18], lower[:18], 10.0},
|
{upper[:18], lower[:18], 10.0},
|
||||||
{digit[:10], digit[:10], 20.0 / 10.0},
|
{digit[:10], digit[:10], 20.0 / 10.0},
|
||||||
{digit[:10], digit[:10], 10.0 / 10.0},
|
{digit[:10], digit[:10], 1.0},
|
||||||
{upper[:24], lower[:24], 20.0 / (10.0 * 24.0)},
|
{upper[:24], lower[:24], 20.0 / (10.0 * 24.0)},
|
||||||
{upper[:24], lower[:24], 10.0 / (10.0 * 24.0)},
|
{upper[:24], lower[:24], 10.0 / (10.0 * 24.0)},
|
||||||
{digit[:10], digit[:10], 20.0 / (10.0 * 24.0 * 10.0)},
|
{digit[:10], digit[:10], 20.0 / (10.0 * 24.0 * 10.0)},
|
||||||
|
|||||||
Reference in New Issue
Block a user