diff --git a/protocol/adsb/fields/bds05.go b/protocol/adsb/fields/bds05.go new file mode 100644 index 0000000..dc4d327 --- /dev/null +++ b/protocol/adsb/fields/bds05.go @@ -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 +} diff --git a/protocol/adsb/fields/bds09.go b/protocol/adsb/fields/bds09.go new file mode 100644 index 0000000..5b7c60b --- /dev/null +++ b/protocol/adsb/fields/bds09.go @@ -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 +} diff --git a/protocol/adsb/fields/bds61.go b/protocol/adsb/fields/bds61.go new file mode 100644 index 0000000..d46872b --- /dev/null +++ b/protocol/adsb/fields/bds61.go @@ -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) +} diff --git a/protocol/adsb/fields/bds62.go b/protocol/adsb/fields/bds62.go new file mode 100644 index 0000000..375e800 --- /dev/null +++ b/protocol/adsb/fields/bds62.go @@ -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) +} diff --git a/protocol/adsb/fields/util.go b/protocol/adsb/fields/util.go new file mode 100644 index 0000000..fb77907 --- /dev/null +++ b/protocol/adsb/fields/util.go @@ -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 +} diff --git a/protocol/adsb/fields/util_test.go b/protocol/adsb/fields/util_test.go new file mode 100644 index 0000000..f2f8229 --- /dev/null +++ b/protocol/adsb/fields/util_test.go @@ -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 +} diff --git a/protocol/adsb/message.go b/protocol/adsb/message.go new file mode 100644 index 0000000..f3b8a97 --- /dev/null +++ b/protocol/adsb/message.go @@ -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', '#', '#', '#', '#', '#', '#', +} diff --git a/protocol/adsb/ra.go b/protocol/adsb/ra.go new file mode 100644 index 0000000..ad5baef --- /dev/null +++ b/protocol/adsb/ra.go @@ -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 + } +} diff --git a/protocol/aprs/base91.go b/protocol/aprs/base91.go index d5b0ec0..779a654 100644 --- a/protocol/aprs/base91.go +++ b/protocol/aprs/base91.go @@ -2,7 +2,6 @@ package aprs import ( "fmt" - "strings" ) func base91Decode(s string) (n int, err error) { @@ -18,6 +17,7 @@ func base91Decode(s string) (n int, err error) { return } +/* func base91Encode(n int) string { var s []string for n > 0 { @@ -27,3 +27,4 @@ func base91Encode(n int) string { } return strings.Join(s, "") } +*/ diff --git a/protocol/aprs/packet.go b/protocol/aprs/packet.go index 0abe39d..8847823 100644 --- a/protocol/aprs/packet.go +++ b/protocol/aprs/packet.go @@ -310,7 +310,7 @@ func (p *Packet) parse() error { return err } p.Position = &pos - p.parseMicEData() + _ = p.parseMicEData() return nil // there is no additional data to parse default: diff --git a/protocol/meshcore/crypto/ed25519.go b/protocol/meshcore/crypto/ed25519.go index e86e868..c15b99c 100644 --- a/protocol/meshcore/crypto/ed25519.go +++ b/protocol/meshcore/crypto/ed25519.go @@ -53,7 +53,7 @@ func (priv *PrivateKey) Sign(_ io.Reader, digest []byte, _ crypto.SignerOpts) (s func (priv *PrivateKey) Public() crypto.PublicKey { p := &PublicKey{} - newPublicKey(p, priv.pub[:]) + _, _ = newPublicKey(p, priv.pub[:]) return p } @@ -118,7 +118,9 @@ func GenerateKey() (*PublicKey, *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) return priv, nil } diff --git a/protocol/meshcore/node.go b/protocol/meshcore/node.go index ef9defd..3f5071b 100644 --- a/protocol/meshcore/node.go +++ b/protocol/meshcore/node.go @@ -357,15 +357,6 @@ func (drv *companionDriver) wait(expect ...byte) ([]byte, error) { 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) { if len(b) < 1 { return // illegal diff --git a/protocol/meshcore/payload.go b/protocol/meshcore/payload.go index 0fcfc79..a4695eb 100644 --- a/protocol/meshcore/payload.go +++ b/protocol/meshcore/payload.go @@ -10,10 +10,7 @@ import ( "git.maze.io/go/ham/protocol/meshcore/crypto" ) -var ( - zeroTime time.Time - zeroPositionBytes = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} -) +var zeroPositionBytes = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} type Payload interface { fmt.Stringer diff --git a/protocol/meshcore/util.go b/protocol/meshcore/util.go index c9c6045..e80abf7 100644 --- a/protocol/meshcore/util.go +++ b/protocol/meshcore/util.go @@ -47,10 +47,6 @@ func decodeTime(b []byte) time.Time { 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 { return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3 } diff --git a/protocol/meshtastic/node.go b/protocol/meshtastic/node.go new file mode 100644 index 0000000..149a3c6 --- /dev/null +++ b/protocol/meshtastic/node.go @@ -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 +} diff --git a/protocol/meshtastic/packet.go b/protocol/meshtastic/packet.go new file mode 100644 index 0000000..b6d6a3f --- /dev/null +++ b/protocol/meshtastic/packet.go @@ -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)) +} diff --git a/util/maidenhead/maidenhead.go b/util/maidenhead/maidenhead.go index cca1998..fe3c1cf 100644 --- a/util/maidenhead/maidenhead.go +++ b/util/maidenhead/maidenhead.go @@ -88,7 +88,7 @@ var parseLocatorMult = []struct { {upper[:18], lower[:18], 20.0}, {upper[:18], lower[:18], 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], 10.0 / (10.0 * 24.0)}, {digit[:10], digit[:10], 20.0 / (10.0 * 24.0 * 10.0)},