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/_attic/data_mice.go b/protocol/aprs/_attic/data_mice.go new file mode 100644 index 0000000..b5010e8 --- /dev/null +++ b/protocol/aprs/_attic/data_mice.go @@ -0,0 +1,335 @@ +package aprs + +import ( + "fmt" + "io" + "math" + "strings" +) + +type MicE struct { + HasMessaging bool `json:"has_messaging"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Altitude float64 `json:"altitude,omitempty"` // Altitude (in meters) + Comment string `json:"comment"` + Symbol string `json:"symbol"` + Velocity *Velocity `json:"velocity,omitempty"` // Velocity encoded in the payload. + Telemetry *Telemetry `json:"telemetry,omitempty"` // Telemetry data +} + +func (m MicE) String() string { + return m.Comment +} + +type micEDecoder struct{} + +func (d micEDecoder) CanDecode(frame Frame) bool { + switch frame.Raw.Type() { + case '`', '\'': + return len(frame.Raw) >= 9 && len(frame.Destination.Call) >= 6 + default: + return false + } +} + +func (d micEDecoder) Decode(frame Frame) (data Data, err error) { + lat, _, longOffset, longDir, err := decodeMicECallsign([]byte(frame.Destination.Call)) + if err != nil { + return nil, err + } + info := []byte(frame.Raw[1:9]) + long := decodeMicELongitude(info[:3], longOffset, longDir) + + pos := &MicE{ + Latitude: float64(lat), + Longitude: float64(long), + } + pos.Symbol = string([]byte{info[7], info[6]}) + pos.Velocity = parseMicECourseAndSpeed(info[3:6]) + + var comment string + if comment, pos.HasMessaging = parseMicERadioModel(string(frame.Raw[9:])); comment != "" { + original := comment + if pos.Altitude, comment, err = parseMicEAltitude(comment); err != nil { + comment = original + } + if pos.Telemetry, comment, err = parseBase91Telemetry(comment); err != nil { + return nil, err + } + pos.Comment = comment + } + + return pos, nil +} + +type micEMessageBit uint8 + +const ( + micEMessageBitZero micEMessageBit = iota + micEMessageBitCustom + micEMessageBitStandard + micEMessageBitInvalid +) + +func decodeMicEMessageBit(v uint8) micEMessageBit { + switch { + case v >= '0' && v <= '9' || v == 'L': + return micEMessageBitZero + case v >= 'A' && v <= 'K': + return micEMessageBitCustom + case v >= 'P' && v <= 'Z': + return micEMessageBitStandard + default: + return micEMessageBitInvalid + } +} + +type micEPositionCommentType uint8 + +const ( + M0 micEPositionCommentType = iota + M1 + M2 + M3 + M4 + M5 + M6 + C0 + C1 + C2 + C3 + C4 + C5 + C6 + Emergency + Invalid +) + +var micEPositionCommentTypeMap = map[micEMessageBit]micEPositionCommentType{ + (0 << 4) | (0 << 2) | 0: Emergency, + (1 << 4) | (1 << 2) | 1: C0, + (1 << 4) | (1 << 2) | 0: C1, + (1 << 4) | (0 << 2) | 1: C2, + (1 << 4) | (0 << 2) | 0: C3, + (0 << 4) | (1 << 2) | 1: C4, + (0 << 4) | (1 << 2) | 0: C5, + (0 << 4) | (0 << 2) | 1: C6, + (2 << 4) | (2 << 2) | 2: M0, + (2 << 4) | (2 << 2) | 0: M1, + (2 << 4) | (0 << 2) | 2: M2, + (2 << 4) | (0 << 2) | 0: M3, + (0 << 4) | (2 << 2) | 2: M4, + (0 << 4) | (2 << 2) | 0: M5, + (0 << 4) | (0 << 2) | 2: M6, +} + +func decodeMicEPositionCommentType(bits []micEMessageBit) micEPositionCommentType { + if v, ok := micEPositionCommentTypeMap[(bits[0]&0x3)<<4|(bits[1]&0x03)<<2|(bits[2]&0x03)]; ok { + return v + } + return Invalid +} + +func decodeMicECallsign(call []byte) (lat Latitude, kind micEPositionCommentType, longOffset int, longDir int, err error) { + if len(call) != 6 { + err = io.ErrUnexpectedEOF + return + } + + var latDir byte = 'X' + if (call[3] >= '0' && call[3] <= '9') || call[3] == 'L' { + latDir = 'S' + } else if call[3] >= 'P' && call[3] <= 'Z' { + latDir = 'N' + } + + latBytes := []byte{ + decodeLatitudeDigit(call[0]), + decodeLatitudeDigit(call[1]), + decodeLatitudeDigit(call[2]), + decodeLatitudeDigit(call[3]), + '.', + decodeLatitudeDigit(call[4]), + decodeLatitudeDigit(call[5]), + latDir, + } + if err = lat.ParseUncompressed(latBytes); err != nil { + return + } + + kind = decodeMicEPositionCommentType([]micEMessageBit{ + decodeMicEMessageBit(call[0]), + decodeMicEMessageBit(call[1]), + decodeMicEMessageBit(call[2]), + }) + + switch { + case (call[4] >= '0' && call[4] <= '9') || call[4] == 'L': + longOffset = 0 + case call[4] >= 'P' && call[4] <= 'Z': + longOffset = 100 + } + + switch { + case (call[5] >= '0' && call[5] <= '9') || call[5] == 'L': + longDir = -1 + case call[5] >= 'P' && call[5] <= 'Z': + longDir = +1 + } + + return +} + +func decodeLatitudeDigit(c uint8) uint8 { + switch { + case c >= '0' && c <= '9': + return c + case c >= 'A' && c <= 'J': + return c - 17 + case c == 'K' || c == 'L' || c == 'Z': + return ' ' + case c >= 'P' && c <= 'Y': + return c - 32 + default: + return 0 + } +} + +func decodeMicELongitude(b []byte, offset, dir int) Longitude { + if len(b) != 3 { + return 0 + } + + d := int(b[0]) - 28 + offset + if d >= 180 && d <= 189 { + d -= 80 + } else if d >= 190 && d <= 199 { + d -= 190 + } + + m := int(b[1] - 28) + if m >= 60 { + m -= 60 + } + + h := int(b[2] - 28) + + return LongitudeFromDMH(d, m, h, dir < 0) +} + +func parseMicECourseAndSpeed(data []byte) (out *Velocity) { + var ( + sp = data[0] - 28 + dc = data[1] - 28 + se = data[2] - 28 + speedKnots = float64(sp)*10 + math.Floor(float64(dc)/10) + courseDeg = ((int(dc) % 10) * 100) + int(se) + ) + if speedKnots >= 800 { + speedKnots -= 800 + } + if courseDeg >= 400 { + courseDeg -= 400 + } + return &Velocity{ + Course: float64(courseDeg), + Speed: knotsToMetersPerSecond(speedKnots), + } +} + +func parseMicEAltitude(data string) (altitude float64, comment string, err error) { + if len(data) < 4 || data[3] != '}' { + return 0, data, nil + } + + var value int + if value, err = base91Decode(data[:3]); err != nil { + return 0, "", fmt.Errorf("aprs: invalid altitude %q: %v", data, err) + } + + altitude = feetToMeters(float64(value - 10000)) + comment = data[4:] + return +} + +func parseMicERadioModel(data string) (stripped string, hasMessaging bool) { + if len(data) == 0 { + return data, false + } + + switch data[0] { + case '>', ']': + stripped = strings.TrimRight(data[1:], "=") // Kenwood TH-D72 / Kenwood TM-D710 + stripped = strings.TrimRight(data[1:], "^") // Kenwood TH-D74 + stripped = strings.TrimRight(data[1:], "&") // Kenwood TH-D75 + case '`', '\'': + hasMessaging = data[0] == '`' + stripped = strings.TrimSuffix(data[1:], "_(") // Yaesu FT2D + stripped = strings.TrimSuffix(data[1:], "_0") // Yaesu FT3D + stripped = strings.TrimSuffix(data[1:], "_3") // Yaesu FT5D + stripped = strings.TrimSuffix(data[1:], "|3") // Byonics TinyTrack 3 + stripped = strings.TrimSuffix(data[1:], "|4") // Byonics TinyTrack 4 + default: + stripped = data + } + return +} + +func parseMicEGridSquare(data string) (latitude, longitude, altitude float64, comment string, err error) { + return +} + +var miceCodes = map[byte]map[int]string{ + '0': {0: "0", 1: "0", 2: "S", 3: "0", 4: "E"}, + '1': {0: "1", 1: "0", 2: "S", 3: "0", 4: "E"}, + '2': {0: "2", 1: "0", 2: "S", 3: "0", 4: "E"}, + '3': {0: "3", 1: "0", 2: "S", 3: "0", 4: "E"}, + '4': {0: "4", 1: "0", 2: "S", 3: "0", 4: "E"}, + '5': {0: "5", 1: "0", 2: "S", 3: "0", 4: "E"}, + '6': {0: "6", 1: "0", 2: "S", 3: "0", 4: "E"}, + '7': {0: "7", 1: "0", 2: "S", 3: "0", 4: "E"}, + '8': {0: "8", 1: "0", 2: "S", 3: "0", 4: "E"}, + '9': {0: "9", 1: "0", 2: "S", 3: "0", 4: "E"}, + 'A': {0: "0", 1: "1 (Custom)"}, + 'B': {0: "1", 1: "1 (Custom)"}, + 'C': {0: "2", 1: "1 (Custom)"}, + 'D': {0: "3", 1: "1 (Custom)"}, + 'E': {0: "4", 1: "1 (Custom)"}, + 'F': {0: "5", 1: "1 (Custom)"}, + 'G': {0: "6", 1: "1 (Custom)"}, + 'H': {0: "7", 1: "1 (Custom)"}, + 'I': {0: "8", 1: "1 (Custom)"}, + 'J': {0: "9", 1: "1 (Custom)"}, + 'K': {0: " ", 1: "1 (Custom)"}, + 'L': {0: " ", 1: "0", 2: "S", 3: "0", 4: "E"}, + 'P': {0: "0", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'Q': {0: "1", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'R': {0: "2", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'S': {0: "3", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'T': {0: "4", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'U': {0: "5", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'V': {0: "6", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'W': {0: "7", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'X': {0: "8", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'Y': {0: "9", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, + 'Z': {0: " ", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"}, +} + +var miceMsgTypes = map[string]string{ + "000": "Emergency", + "001 (Std)": "Priority", + "001 (Custom)": "Custom-6", + "010 (Std)": "Special", + "010 (Custom)": "Custom-5", + "011 (Std)": "Committed", + "011 (Custom)": "Custom-4", + "100 (Std)": "Returning", + "100 (Custom)": "Custom-3", + "101 (Std)": "In Service", + "101 (Custom)": "Custom-2", + "110 (Std)": "En Route", + "110 (Custom)": "Custom-1", + "111 (Std)": "Off Duty", + "111 (Custom)": "Custom-0", +} diff --git a/protocol/aprs/_attic/position.go b/protocol/aprs/_attic/position.go new file mode 100644 index 0000000..0b34f9c --- /dev/null +++ b/protocol/aprs/_attic/position.go @@ -0,0 +1,219 @@ +package aprs + +import ( + "errors" + "fmt" +) + +var ( + ErrLatitude = errors.New("aprs: invalid latitude") + ErrLongitude = errors.New("aprs: invalid longitude") +) + +// Latitude is the north-south position. Positive values are North, negative are South. +type Latitude float64 + +func LatitudeFromDMH(degrees, minutes, hundreths int, north bool) Latitude { + v := float64(degrees) + float64(minutes)/60 + float64(hundreths)/6000 + + for v > 90 { + v -= 180 + } + for v < -90 { + v += 180 + } + + if north { + return Latitude(v) + } + return -Latitude(v) +} + +func (lat Latitude) DMH() (degrees, minutes, hundreths int, north bool) { + degrees = int(lat) + minutes = int((float64(lat) - float64(degrees)) * 60) + hundreths = int((float64(lat) - float64(degrees) - float64(minutes)/60) * 6000) + + if hundreths == 100 { + hundreths = 0 + minutes += 1 + } + + if minutes == 60 { + minutes = 0 + degrees += 1 + } + + north = lat >= 0 + return +} + +func (lat *Latitude) ParseCompressed(b []byte) error { + if len(b) != 4 { + return ErrLatitude + } + + n, err := base91Decode(string(b)) + if err != nil { + return err + } + + *lat = Latitude(90 - float64(n)/380926) + return nil +} + +func (lat *Latitude) ParseUncompressed(b []byte) error { + if len(b) != 8 || b[4] != '.' { + return ErrLatitude + } + + var north bool + switch b[7] { + case 'N': + north = true + case 'S': + north = false + default: + return ErrLatitude + } + + var ( + degrees, minutes, hundreths int + err error + ) + if degrees, err = parseBytesWithSpaces(b[0:2]); err != nil { + return err + } + if minutes, err = parseBytesWithSpaces(b[2:4]); err != nil { + return err + } + if hundreths, err = parseBytesWithSpaces(b[5:7]); err != nil { + return err + } + + *lat = LatitudeFromDMH(degrees, minutes, hundreths, north) + + return nil +} + +func (lat Latitude) Compressed(b []byte) { + v := int((90 - float64(lat)) * 380926.0) + base91Encode(b, v) +} + +func (lat Latitude) Uncompressed(b []byte) { + var ( + degrees, minutes, hundreths, north = lat.DMH() + v = fmt.Sprintf("%02d%02d.%02d", degrees, minutes, hundreths) + ) + if north { + v += "N" + } else { + v += "S" + } + copy(b, []byte(b)) +} + +// Longitude is the east-west position. Positive values are East, negative are West. +type Longitude float64 + +func LongitudeFromDMH(degrees, minutes, hundreths int, east bool) Longitude { + v := float64(degrees) + float64(minutes)/60 + float64(hundreths)/6000 + + for v > 180 { + v -= 360 + } + for v < -180 { + v += 360 + } + + if east { + return Longitude(v) + } + return -Longitude(v) +} + +func (long Longitude) DMH() (degrees, minutes, hundreths int, east bool) { + degrees = int(long) + minutes = int((float64(long) - float64(degrees)) * 60) + hundreths = int((float64(long) - float64(degrees) - float64(minutes)/60) * 6000) + + if hundreths == 100 { + hundreths = 0 + minutes += 1 + } + + if minutes == 60 { + minutes = 0 + degrees += 1 + } + + east = long >= 0 + return +} + +func (long *Longitude) ParseCompressed(b []byte) error { + if len(b) != 4 { + return ErrLatitude + } + + n, err := base91Decode(string(b)) + if err != nil { + return err + } + + *long = Longitude(float64(n)/190463.0 - 180) + return nil +} + +func (long *Longitude) ParseUncompressed(b []byte) error { + if len(b) != 9 || b[5] != '.' { + return ErrLongitude + } + + var east bool + switch b[8] { + case 'E': + east = true + case 'W': + east = false + default: + return ErrLongitude + } + + var ( + degrees, minutes, hundreths int + err error + ) + if degrees, err = parseBytesWithSpaces(b[0:3]); err != nil { + return err + } + if minutes, err = parseBytesWithSpaces(b[3:5]); err != nil { + return err + } + if hundreths, err = parseBytesWithSpaces(b[6:8]); err != nil { + return err + } + + *long = LongitudeFromDMH(degrees, minutes, hundreths, east) + + return nil +} + +func (long Longitude) Compressed(b []byte) { + v := int((180 + float64(long)) * 190463) + base91Encode(b, v) +} + +func (long Longitude) Uncompressed(b []byte) { + var ( + degrees, minutes, hundreths, east = long.DMH() + v = fmt.Sprintf("%03d%02d.%02d", degrees, minutes, hundreths) + ) + if east { + v += "E" + } else { + v += "W" + } + copy(b, []byte(b)) +} diff --git a/protocol/aprs/_attic/position_test.go b/protocol/aprs/_attic/position_test.go new file mode 100644 index 0000000..d91784e --- /dev/null +++ b/protocol/aprs/_attic/position_test.go @@ -0,0 +1,53 @@ +package aprs + +import "testing" + +func TestLatitude(t *testing.T) { + tests := []struct { + Test string + Want Latitude + }{ + {"4903.50N", 49.05833333333333}, + {"4903.50S", -49.05833333333333}, + {"4903.5 S", -49.05833333333333}, + {"4903. S", -49.05}, + {"490 . S", -49}, + {"4 . S", -40}, + } + for _, test := range tests { + t.Run(test.Test, func(t *testing.T) { + var lat Latitude + if err := lat.ParseUncompressed([]byte(test.Test)); err != nil { + t.Fatal(err) + } + if !testAlmostEqual(float64(test.Want), float64(lat)) { + t.Errorf("expected %f, got %f", test.Want, lat) + } + }) + } +} + +func TestLongitude(t *testing.T) { + tests := []struct { + Test string + Want Longitude + }{ + {"00000.00E", 0}, + {"00000.00W", 0}, + {"00000.98W", -0.016333}, + {"00098. W", -1.633333}, + {"098 . W", -98.000000}, + {"9 . W", -180.000000}, + } + for _, test := range tests { + t.Run(test.Test, func(t *testing.T) { + var long Longitude + if err := long.ParseUncompressed([]byte(test.Test)); err != nil { + t.Fatal(err) + } + if !testAlmostEqual(float64(test.Want), float64(long)) { + t.Errorf("expected %f, got %f", test.Want, long) + } + }) + } +} diff --git a/protocol/aprs/aprsis/proxy.go b/protocol/aprs/aprsis/proxy.go index e15fc4d..d374daa 100644 --- a/protocol/aprs/aprsis/proxy.go +++ b/protocol/aprs/aprsis/proxy.go @@ -217,6 +217,7 @@ func (client *ProxyClient) copy(dst, src net.Conn, host, dir string, call *strin func (client *ProxyClient) Info() *radio.Info { // We have very little information actually, but here we go: return &radio.Info{ + ID: client.myCall, Name: client.myCall, } } diff --git a/protocol/meshcore/node.go b/protocol/meshcore/node.go index f758c48..e6c92a3 100644 --- a/protocol/meshcore/node.go +++ b/protocol/meshcore/node.go @@ -32,8 +32,8 @@ func NewCompanion(conn io.ReadWriteCloser) (*Node, error) { }, nil } -func NewRepeater(conn io.ReadWriteCloser, hasSNR bool) (*Node, error) { - driver := newRepeaterDriver(conn, hasSNR) +func NewRepeater(conn io.ReadWriteCloser) (*Node, error) { + driver := newRepeaterDriver(conn) if err := driver.Setup(); err != nil { return nil, err @@ -60,7 +60,7 @@ func (dev *Node) Info() *radio.Info { return dev.driver.Info() } -func (dev *Node) Stats() map[string]any { +func (dev *Node) Stats() <-chan map[string]any { return dev.driver.Stats() } @@ -78,7 +78,7 @@ type nodeDriver interface { Setup() error Packets() <-chan *Packet - Stats() map[string]any + Stats() <-chan map[string]any } type nodeTracer interface { diff --git a/protocol/meshcore/node_companion.go b/protocol/meshcore/node_companion.go index 09b5c50..274fa37 100644 --- a/protocol/meshcore/node_companion.go +++ b/protocol/meshcore/node_companion.go @@ -47,7 +47,7 @@ type companionDriver struct { info companionInfo traceTag uint32 traceAuthCode uint32 - stats map[string]any + stats chan map[string]any } type companionDriverWaiting struct { @@ -131,6 +131,7 @@ func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver { conn: conn, waiting: make(chan *companionDriverWaiting, 16), traceTag: rand.Uint32(), + stats: make(chan map[string]any, 2), //traceAuthCode: rand.Uint32(), } } @@ -144,7 +145,7 @@ func (drv *companionDriver) Setup() (err error) { if err = drv.sendAppStart(); err != nil { return } - if err = drv.sendDeviceInfo(); err != nil { + if err = drv.getDeviceInfo(); err != nil { return } return @@ -209,7 +210,7 @@ func (drv *companionDriver) Info() *radio.Info { } } -func (drv *companionDriver) Stats() map[string]any { +func (drv *companionDriver) Stats() <-chan map[string]any { return drv.stats } @@ -397,7 +398,7 @@ func (drv *companionDriver) sendAppStart() (err error) { return } -func (drv *companionDriver) sendDeviceInfo() (err error) { +func (drv *companionDriver) getDeviceInfo() (err error) { var ( args = []byte{0x03} data []byte @@ -425,6 +426,11 @@ func (drv *companionDriver) sendDeviceInfo() (err error) { return } +func (drv *companionDriver) getPublicKey() (err error) { + // TODO + return err +} + func (drv *companionDriver) poll() { for { frame, err := drv.readFrame() diff --git a/protocol/meshcore/node_repeater.go b/protocol/meshcore/node_repeater.go index b66adb0..2d29319 100644 --- a/protocol/meshcore/node_repeater.go +++ b/protocol/meshcore/node_repeater.go @@ -3,6 +3,7 @@ package meshcore import ( "bufio" "encoding/hex" + "encoding/json" "fmt" "io" "math" @@ -23,7 +24,7 @@ type repeaterDriver struct { lastFrame []byte lastFrameAt time.Time info repeaterInfo - stats map[string]any + stats chan map[string]any err error } @@ -78,7 +79,7 @@ type repeaterInfo struct { Type NodeType Power byte // in dBm MaxPower byte // in dBm - PublicKey [32]byte + PublicKey string Latitude float64 Longitude float64 HasMultiACKs bool @@ -98,15 +99,20 @@ type repeaterInfo struct { Manufacturer string } -func newRepeaterDriver(conn io.ReadWriteCloser, hasSNR bool) *repeaterDriver { +func newRepeaterDriver(conn io.ReadWriteCloser) *repeaterDriver { return &repeaterDriver{ conn: conn, waiting: make(chan *repeaterDriverWaiting, 16), - hasSNR: hasSNR, - stats: make(map[string]any), + stats: make(chan map[string]any, 2), } } +func newRepeaterDriverWithSNRPatch(conn io.ReadWriteCloser) *repeaterDriver { + driver := newRepeaterDriver(conn) + driver.hasSNR = true + return driver +} + func (drv *repeaterDriver) Close() error { return drv.conn.Close() } @@ -150,6 +156,7 @@ func (drv *repeaterDriver) Info() *radio.Info { } return &radio.Info{ + ID: drv.info.PublicKey, Name: drv.info.Name, Manufacturer: manufacturer, Device: device, @@ -179,7 +186,7 @@ func (drv *repeaterDriver) RawPackets() <-chan *protocol.Packet { return drv.rawPackets } -func (drv *repeaterDriver) Stats() map[string]any { +func (drv *repeaterDriver) Stats() <-chan map[string]any { return drv.stats } @@ -193,6 +200,11 @@ func (drv *repeaterDriver) queryDeviceInfo() (err error) { return } + // Fetch public key + if drv.info.PublicKey, err = drv.writeCommand("get", "public.key"); err != nil { + return + } + var line string // Fetch frequency, bandwidth and LoRa settings @@ -412,24 +424,26 @@ func (drv *repeaterDriver) poll() { } func (drv *repeaterDriver) pollStats() { - ticker := time.NewTicker(time.Minute) + ticker := time.NewTicker(time.Second * 10) defer ticker.Stop() for { + stats := make(map[string]any) + neighbors, err := drv.getNeighbors() if err != nil { Logger.Warnf("meshcore: failed to get neighbors: %v", err) } else { - drv.stats["neighbors"] = neighbors + stats["neighbors"] = neighbors } - response, err := drv.writeCommand("stats") + response, err := drv.writeCommand("stats-core") if err != nil { Logger.Warnf("meshcore: failed to get stats: %v", err) return } - stats := make(map[string]any) + neighborStats := make(map[string]any) for _, line := range strings.Split(response, "\n") { parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { @@ -439,15 +453,53 @@ func (drv *repeaterDriver) pollStats() { value := parts[1] if i, err := strconv.Atoi(value); err == nil { - stats[key] = i + neighborStats[key] = i } else if f, err := strconv.ParseFloat(value, 64); err == nil { - stats[key] = f + neighborStats[key] = f } else { - stats[key] = value + neighborStats[key] = value } } - drv.stats = stats + var ( + coreStats = make(map[string]any) + radioStats = make(map[string]any) + packetStats = make(map[string]any) + ) + if response, err := drv.writeCommand("stats-core"); err == nil { + if err = json.Unmarshal([]byte(response), &coreStats); err != nil { + Logger.Warnf("meshcore: failed to decode core stats: %v", err) + } + } else { + Logger.Warnf("meshcore: failed to get core stats: %v", err) + } + + if response, err := drv.writeCommand("stats-radio"); err == nil { + if err = json.Unmarshal([]byte(response), &radioStats); err != nil { + Logger.Warnf("meshcore: failed to decode radio stats: %v", err) + } + } else { + Logger.Warnf("meshcore: failed to get radio stats: %v", err) + } + + if response, err := drv.writeCommand("stats-packets"); err == nil { + if err = json.Unmarshal([]byte(response), &packetStats); err != nil { + Logger.Warnf("meshcore: failed to decode packet stats: %v", err) + } + } else { + Logger.Warnf("meshcore: failed to get packet stats: %v", err) + } + + stats["neighbors"] = neighborStats + stats["core"] = coreStats + stats["radio"] = radioStats + stats["packets"] = packetStats + + select { + case drv.stats <- stats: + default: + Logger.Warn("meshcore: stats channel full, dropping stats") + } <-ticker.C }