Added Radio.ID and refactored the Stats interface
Some checks failed
Run tests / test (1.25) (push) Failing after 1m0s
Run tests / test (stable) (push) Failing after 1m0s

This commit is contained in:
2026-03-17 08:33:06 +01:00
parent 8ec85821e4
commit 27e2da1943
15 changed files with 2045 additions and 22 deletions

View File

@@ -0,0 +1,158 @@
package fields
// SurveillanceStatus is the surveillance status
//
// Specified in Doc 9871 / A.2.3.2.6
type SurveillanceStatus byte
const (
SSNoCondition SurveillanceStatus = iota // No aircraft category information
SSPermanentAlert // Permanent alert (emergency condition)
SSTemporaryAlert // Temporary alert (change in Mode A identity code other than emergency condition)
SSSPICondition // SPI condition
)
// AltitudeSource is the type of source of the Altitude: Barometric or GNSS.
type AltitudeSource byte
const (
AltitudeBarometric AltitudeSource = iota // Altitude is barometric altitude
AltitudeGNSS // Altitude is GNSS height (HAE)
)
// AltitudeReportMethod defines how the altitude is reported.
type AltitudeReportMethod byte
const (
AltitudeReport100FootIncrements AltitudeReportMethod = iota // 100-foot increments
AltitudeReport25FootIncrements // 25-foot increments
)
// CompactPositionReportingFormat is the CPR (Compact Position Reporting) format definition
//
// Specified in Doc 9871 / A.2.3.3.3
type CompactPositionReportingFormat byte
const (
CPRFormatEven CompactPositionReportingFormat = iota // Even format coding
CPRFormatOdd // Odd format coding
)
func ParseCompactPositioningReportFormat(data []byte) CompactPositionReportingFormat {
bits := (data[2] & 0x04) >> 2
return CompactPositionReportingFormat(bits)
}
func ParseAltitude(data []byte) (altitude int32, source AltitudeSource, method AltitudeReportMethod, err error) {
source = AltitudeBarometric
if format := (data[0] & 0xF8) >> 3; 20 <= format && format <= 22 {
source = AltitudeGNSS
}
// Altitude code is a 12 bits fields, so read a uint16
// bit | 8 9 10 11 12 13 14 15| 16 17 18 19 20 21 22 23 |
// message | x x x x x x x x| x x x x _ _ _ _ |
// 100 foot |C1 A1 C2 A2 C4 A4 B1 Q| B2 D2 B4 D4 _ _ _ _ |
// Get the Q bit
if qBit := (data[1] & 0x01) != 0; qBit {
// If the Q bit equals 1, the 11-bit field represented by bits 8 to 14 and 16 to 18
n := uint16(0)
n |= uint16(data[1]&0xFE) << 3
n |= uint16(data[2]&0xF0) >> 4
return 25*int32(n) - 1000, source, AltitudeReport25FootIncrements, nil
}
// Otherwise, altitude is reported in 100 foot increments
c1 := (data[1] & 0x80) != 0
a1 := (data[1] & 0x40) != 0
c2 := (data[1] & 0x20) != 0
a2 := (data[1] & 0x10) != 0
c4 := (data[1] & 0x08) != 0
a4 := (data[1] & 0x04) != 0
b1 := (data[1] & 0x02) != 0
b2 := (data[2] & 0x80) != 0
d2 := (data[2] & 0x40) != 0
b4 := (data[2] & 0x20) != 0
d4 := (data[2] & 0x10) != 0
if altitude, err = GillhamToAltitude(false, d2, d4, a1, a2, a4, b1, b2, b4, c1, c2, c4); err != nil {
return
}
return
}
// MovementStatus is the status of the Movement information
type MovementStatus int
const (
// MSNoInformation indicates no information
MSNoInformation MovementStatus = 0 // No information
MSAircraftStopped MovementStatus = 1 // The aircraft is stopped
MSAboveMaximum MovementStatus = 124 // The Movement is above the maximum
MSReservedDecelerating MovementStatus = 125 // Reserved
MSReservedAccelerating MovementStatus = 126 // Reserved
MSReservedBackingUp MovementStatus = 127 // Reserved
)
func ParseMovementStatus(data []byte) (float64, MovementStatus) {
bits := (data[0]&0x07)<<4 + (data[1]&0xF0)>>4
status := MovementStatus(bits)
return status.Speed(), status
}
// Speed in knots.
func (status MovementStatus) Speed() float64 {
if status == 0 || status == 1 || status > 124 {
return 0
} else if 2 <= status && status <= 8 {
return 0.125 + float64(status-2)*0.125
} else if 9 <= status && status <= 12 {
return 1 + float64(status-9)*0.25
} else if 13 <= status && status <= 38 {
return 2 + float64(status-13)*0.5
} else if 39 <= status && status <= 93 {
return 15 + float64(status-39)*1.0
} else if 94 <= status && status <= 108 {
return 70 + float64(status-94)*2.0
} else if 109 <= status && status <= 123 {
return 100 + float64(status-109)*5.0
}
// Movement max
return 175
}
// EncodedLatitude is the encoded latitude
//
// Specified in Doc 9871 / C.2.6
type EncodedLatitude uint32
func ParseEncodedLatitude(data []byte) EncodedLatitude {
return EncodedLatitude((data[2]&0x02)>>1)<<16 |
EncodedLatitude((data[2]&0x01)<<7+(data[3]&0xFE)>>1)<<8 |
EncodedLatitude((data[3]&0x01)<<7+(data[4]&0xFE)>>1)
}
// EncodedLongitude is the encoded longitude
//
// Specified in Doc 9871 / C.2.6
type EncodedLongitude uint32
func ParseEncodedLongitude(data []byte) EncodedLongitude {
return EncodedLongitude(data[4]&0x01)<<16 |
EncodedLongitude(data[5])<<8 |
EncodedLongitude(data[6])
}
func ParseGroundTrackStatus(data []byte) (track float64, status bool) {
status = (data[1] & 0x08) != 0
allBits := (uint16(data[1]&0x07)<<8 | uint16(data[2]&0xF0)) >> 4
track = float64(allBits) * 360.0 / 128.0
return
}
func ParseTimeSynchronizedToUTC(data []byte) bool {
return ((data[2] & 0x08) >> 3) != 0
}

View File

@@ -0,0 +1,208 @@
package fields
// NumericValueStatus is the status of the numeric values, such as air speed, velocity, etc.
type NumericValueStatus byte
const (
NVSNoInformation NumericValueStatus = iota // No velocity information
NVSRegular // Velocity is computed on the linear scale value of field * factor
NVSMaximum // Velocity field value indicates velocity greater the maximum of the scale
)
// ParseHeightDifference reads the Height difference from a 56 bits data field
func ParseHeightDifference(data []byte) (int16, NumericValueStatus) {
negative := data[6]&0x80 != 0
difference := int16(data[6] & 0x7F)
if difference == 0 {
return 0, NVSNoInformation
} else if difference >= 127 {
if negative {
return -3150, NVSMaximum
} else {
return 3150, NVSMaximum
}
}
difference = (difference - 1) * 25
if negative {
difference = -difference
}
return difference, NVSRegular
}
// VerticalRateSource is the Source Bit for Vertical Rate definition
//
// Specified in Doc 9871 / Table A-2-9
type VerticalRateSource byte
const (
VerticalRateSourceGNSS VerticalRateSource = iota // GNSS source
VerticalRateSourceBarometric // Barometric source
)
// DirectionNorthSouth is the Direction Bit NS Velocity definition
//
// Specified in Doc 9871 / Table A-2-9
type DirectionNorthSouth byte
func ParseDirectionNorthSouth(data []byte) DirectionNorthSouth {
return DirectionNorthSouth((data[3] & 0x80) >> 7)
}
func ParseVelocityNorthSouthNormal(data []byte) (velocity uint16, status NumericValueStatus) {
velocity = (uint16(data[3]&0x7F)<<8 | uint16(data[4]&0xE0)) >> 5
if velocity == 0 {
return 0, NVSNoInformation
} else if velocity >= 1023 {
return 1023, NVSMaximum
}
return velocity - 1, NVSRegular
}
func ParseVelocityNorthSouthSupersonic(data []byte) (velocity uint16, status NumericValueStatus) {
velocity = (uint16(data[3]&0x7F)<<8 | uint16(data[4]&0xE0)) >> 5
if velocity == 0 {
return 0, NVSNoInformation
} else if velocity >= 1023 {
return 4088, NVSMaximum
}
return (velocity - 1) * 4, NVSRegular
}
const (
DNSNorth DirectionNorthSouth = iota // North
DNSSouth // South
)
// DirectionEastWest is the Direction Bit EW Velocity definition
//
// Specified in Doc 9871 / Table A-2-9
type DirectionEastWest byte
func ParseDirectionEastWest(data []byte) DirectionEastWest {
return DirectionEastWest((data[1] & 0x04) >> 2)
}
func ParseVelocityEastWestNormal(data []byte) (velocity uint16, status NumericValueStatus) {
velocity = (uint16(data[1]&0x03) | uint16(data[2]))
if velocity == 0 {
return 0, NVSNoInformation
} else if velocity >= 1023 {
return 1023, NVSMaximum
}
return velocity - 1, NVSRegular
}
func ParseVelocityEastWestSupersonic(data []byte) (velocity uint16, status NumericValueStatus) {
velocity = (uint16(data[1]&0x03) | uint16(data[2]))
if velocity == 0 {
return 0, NVSNoInformation
} else if velocity >= 1023 {
return 4088, NVSMaximum
}
return (velocity - 1) * 4, NVSRegular
}
const (
DEWEast DirectionEastWest = iota // East
DEWWest // West
)
func ParseIntentChange(data []byte) bool {
return (data[1]&0x80)>>7 != 0
}
func ParseIFRCapability(data []byte) bool {
return (data[1]&0x40)>>6 != 0
}
// NavigationUncertaintyCategory is the Navigation Uncertainty Category definition
//
// Specified in Doc 9871 / Table A-2-9
type NavigationUncertaintyCategory byte
const (
NUCPUnknown NavigationUncertaintyCategory = iota // Unknown
NUCPHorizontalLowerThan10VerticalLowerThan15Point2 // Horizontal < 10m/s and Vertical < 15.2m/s
NUCPHorizontalLowerThan3VerticalLowerThan4Point6 // Horizontal < 3m/s and Vertical < 4.6m/s
NUCPHorizontalLowerThan1VerticalLowerThan1Point5 // Horizontal < 1m/s and Vertical < 1.5m/s
NUCPHorizontalLowerThan0Point3VerticalLowerThan0Point46 // Horizontal < 0.3m/s and Vertical < 0.46m/s
)
func ParseeNavigationUncertaintyCategory(data []byte) NavigationUncertaintyCategory {
return NavigationUncertaintyCategory((data[1] & 0x38) >> 3)
}
func ParseMagneticHeading(data []byte) (heading float64, status bool) {
status = (data[1]&0x04)>>2 != 0
value := uint16(data[1]&0x03)<<8 | uint16(data[2])
heading = float64(value) * 360 / 1024.0
return
}
func ParseAirspeedNormal(data []byte) (speed uint16, status NumericValueStatus) {
velocity := (uint16(data[3]&0x7F)<<8 | uint16(data[4]&0xE0)) >> 5
if velocity == 0 {
return 0, NVSNoInformation
} else if velocity >= 1023 {
return 1023, NVSMaximum
}
return velocity - 1, NVSRegular
}
func ParseAirspeedSupersonic(data []byte) (speed uint16, status NumericValueStatus) {
velocity := (uint16(data[3]&0x7F)<<8 | uint16(data[4]&0xE0)) >> 5
if velocity == 0 {
return 0, NVSNoInformation
} else if velocity >= 1023 {
return 4088, NVSMaximum
}
return (velocity - 1) * 4, NVSRegular
}
func ParseVerticalRateSource(data []byte) VerticalRateSource {
return VerticalRateSource((data[4] & 0x10) >> 4)
}
func ParseVerticalRate(data []byte) (rate int16, status NumericValueStatus) {
negative := data[4]&0x08 != 0
rate = int16(uint16(data[4]&0x07)<<8 | uint16(data[5]&0xFC))
if rate == 0 {
return 0, NVSNoInformation
} else if rate >= 511 {
if negative {
return -32640, NVSMaximum
}
return 32640, NVSMaximum
}
rate = (rate - 1) * 64
if negative {
rate = -rate
}
return rate, NVSRegular
}
func ParseHeightDifferenceFromBaro(data []byte) (difference int16, status NumericValueStatus) {
negative := data[6]&0x80 != 0
difference = int16(data[6] & 0x7F)
if difference == 0 {
return 0, NVSNoInformation
} else if difference >= 127 {
if negative {
return -3150, NVSMaximum
} else {
return 3150, NVSMaximum
}
}
difference = (difference - 1) * 25
if negative {
difference = -difference
}
return difference, NVSRegular
}

View File

@@ -0,0 +1,21 @@
package fields
// EmergencyPriorityStatus is the Emergency Priority Status definition
//
// Specified in Doc 9871 / Table B-2-97a
type EmergencyPriorityStatus byte
const (
EPSNoEmergency EmergencyPriorityStatus = iota // No emergency
EPSGeneralEmergency // General emergency
EPSLifeguardMedical // Lifeguard/medical emergency
EPSMinimumFuel // Minimum fuel
EPSNoCommunication // No communications
EPSUnlawfulInterference // Unlawful interference
EPSDownedAircraft // Downed aircraft
EPSReserved7 // Reserved
)
func ParseEmergencyPriorityStatus(data []byte) EmergencyPriorityStatus {
return EmergencyPriorityStatus((data[1] & 0xE0) >> 5)
}

View File

@@ -0,0 +1,176 @@
package fields
// VerticalDataAvailableSourceIndicator is the Vertical Data Available / Source Indicator definition
//
// Specified in Doc 9871 / B.2.3.9.3
type VerticalDataAvailableSourceIndicator byte
const (
VDANoValidDataAvailable VerticalDataAvailableSourceIndicator = iota // No data available
VDAAutopilot // Autopilot control panel selected value, such as Mode Control Panel (MCP) or Flight Control Unit (FCU)
VDAHoldingAltitude // Holding altitude
VDAFMS // FMS/RNAV system
)
func ParseVerticalDataAvailableSourceIndicator(data []byte) VerticalDataAvailableSourceIndicator {
return VerticalDataAvailableSourceIndicator((data[0]&0x01)<<1 + (data[1]&0x80)>>7)
}
// TargetAltitudeType is the Target Altitude Type definition
//
// Specified in Doc 9871 / B.2.3.9.4
type TargetAltitudeType byte
const (
TATReferencedToPressureAltitude TargetAltitudeType = iota // Referenced to pressure-altitude (flight level)
TATReferencedToBarometricAltitude // Referenced to barometric corrected altitude (mean sea level)
)
// ReadTargetAltitudeType reads the TargetAltitudeType from a 56 bits data field
func ParseTargetAltitudeType(data []byte) TargetAltitudeType {
return TargetAltitudeType((data[1] & 0x40) >> 6)
}
// TargetAltitudeCapability is the Target Altitude Capability definition
//
// Specified in Doc 9871 / B.2.3.9.5
type TargetAltitudeCapability byte
const (
TACAltitudeOnly TargetAltitudeCapability = iota // Holding altitude only
TACAltitudeOrAutopilot // Either holding altitude or autopilot control panel selected altitude
TACAltitudeOrAutopilotOrFMS // Either holding altitude, autopilot control panel selected altitude, or any FMS/RNAV level-off altitude
TACReserved3 // Reserved
)
// ParseTargetAltitudeCapability reads the TargetAltitudeCapability from a 56 bits data field
func ParseTargetAltitudeCapability(data []byte) TargetAltitudeCapability {
return TargetAltitudeCapability((data[1] & 0x18) >> 3)
}
// VerticalModeIndicator is the Vertical Mode Indicator definition
//
// Specified in Doc 9871 / B.2.3.9.6
type VerticalModeIndicator byte
const (
VMIUnknown VerticalModeIndicator = iota // Unknown mode or information unavailable
VMIAcquiringMode // Acquiring Mode
VMICapturingMode // Capturing or Maintaining Mode
VMIReserved3 // Reserved
)
// ParseVerticalModeIndicator reads the VerticalModeIndicator from a 56 bits data field
func ParseVerticalModeIndicator(data []byte) VerticalModeIndicator {
return VerticalModeIndicator((data[1] & 0x06) >> 1)
}
// HorizontalDataAvailableSourceIndicator is the Horizontal Data Available / Source Indicator definition
//
// Specified in Doc 9871 / B.2.3.9.8
type HorizontalDataAvailableSourceIndicator byte
const (
HDANoValidDataAvailable HorizontalDataAvailableSourceIndicator = iota // No data available
HDAAutopilot // Autopilot control panel selected value, such as Mode Control Panel (MCP) or Flight Control Unit (FCU)
HDAHoldingAltitude // Maintaining current heading or track angle (e.g. autopilot mode select)
HDAFMS // FMS/RNAV system (indicates track angle specified by leg type)
)
// ParseHorizontalDataAvailableSourceIndicator reads the HorizontalDataAvailableSourceIndicator from a 56 bits data field
func ParseHorizontalDataAvailableSourceIndicator(data []byte) HorizontalDataAvailableSourceIndicator {
return HorizontalDataAvailableSourceIndicator((data[3] & 0x60) >> 5)
}
// TargetHeadingTrackIndicator is the Target Heading / Track Angle Indicator definition
//
// Specified in Doc 9871 / B.2.3.9.10
type TargetHeadingTrackIndicator byte
const (
TargetTrackHeadingAngle TargetHeadingTrackIndicator = iota // Target heading angle is being reported
TargetTrackAngle // Track angle is being reported
)
// ParseTargetHeadingTrackIndicator reads the TargetHeadingTrackIndicator from a 56 bits data field
func ParseTargetHeadingTrackIndicator(data []byte) TargetHeadingTrackIndicator {
return TargetHeadingTrackIndicator((data[4] & 0x08) >> 3)
}
// ReadTargetHeadingTrackAngle reads the TargetAltitude from a 56 bits data field
// Specified in Doc 9871 / B.2.3.9.9
func ReadTargetHeadingTrackAngle(data []byte) (uint16, NumericValueStatus) {
heading := (uint16(data[3]&0x1F)<<8 | uint16(data[2]&0xF0)) >> 4
if heading > 359 {
return 0, NVSMaximum
}
return heading, NVSRegular
}
// HorizontalModeIndicator is the Horizontal Mode Indicator definition
//
// Specified in Doc 9871 / B.2.3.9.11
type HorizontalModeIndicator byte
const (
HMIUnknown HorizontalModeIndicator = iota // Unknown mode or information unavailable
HMIAcquiringMode // Acquiring Mode
HMICapturingMode // Capturing or Maintaining Mode
HMIReserved3 // Reserved
)
// ParseHorizontalModeIndicator reads the HorizontalModeIndicator from a 56 bits data field
func ParseHorizontalModeIndicator(data []byte) HorizontalModeIndicator {
return HorizontalModeIndicator((data[4] & 0x06) >> 1)
}
// NavigationalAccuracyCategoryPositionV1 is the Navigational Accuracy Category Position definition
//
// Specified in Doc 9871 / B.2.3.9.12
type NavigationalAccuracyCategoryPositionV1 byte
const (
NACPV1EPUGreaterThan18Point52Km NavigationalAccuracyCategoryPositionV1 = iota // EPU >= 18.52 km (10 NM) - Unknown accuracy
NACPV1EPULowerThan18Point52Km // EPU < 18.52 km (10 NM) - RNP-10 accuracy
NACPV1EPULowerThan7Point408Km // EPU < 7.408 km (4 NM) - RNP-4 accuracy
NACPV1EPULowerThan3Point704Km // EPU < 3.704 km (2 NM) - RNP-2 accuracy
NACPV1EPUGreaterThan1852M // EPU < 1 852 m (1 NM) - RNP-1 accuracy
NACPV1EPULowerThan926M // EPU < 926 m (0.5 NM) - RNP-0.5 accuracy
NACPV1EPUGreaterThan555Point6M // EPU < 555.6 m ( 0.3 NM) - RNP-0.3 accuracy
NACPV1EPULowerThan185Point2M // EPU < 185.2 m (0.1 NM) - RNP-0.1 accuracy
NACPV1EPUGreaterThan92Point6M // EPU < 92.6 m (0.05 NM) - e.g. GPS (with SA)
NACPV1EPULowerThan30MAndVEPULowerThan45M // EPU < 30 m and VEPU < 45 m - e.g. GPS (SA off)
NACPV1EPULowerThan10MAndVEPULowerThan15M // EPU < 10 m and VEPU < 15 m - e.g. WAAS
NACPV1EPULowerThan4MAndVEPULowerThan3M // EPU < 3 m and VEPU < 4 m - e.g. LAAS
NACPV1Reserved12 // Reserved
NACPV1Reserved13 // Reserved
NACPV1Reserved14 // Reserved
NACPV1Reserved15 // Reserved
)
// ParseNavigationalAccuracyCategoryPositionV1 reads the NavigationalAccuracyCategoryPositionV1 from a 56 bits data field
func ParseNavigationalAccuracyCategoryPositionV1(data []byte) NavigationalAccuracyCategoryPositionV1 {
return NavigationalAccuracyCategoryPositionV1((data[4]&0x01)<<3 + (data[5]&0xE0)>>5)
}
// NICBaro is the NIC Baro definition
//
// Specified in Doc 9871 / B.2.3.9.13
type NICBaro byte
const (
// NICBGilhamNotCrossChecked indicates that the barometric altitude that is being reported in the Airborne
// Position Message is based on a Gilham coded input that has not been cross-checked against another source of
// pressure-altitude
NICBGilhamNotCrossChecked NICBaro = iota
// NICBGilhamCrossCheckedOrNonGilham indicates that the barometric altitude that is being reported in the Airborne
// Position Message is either based on a Gilham code input that has been cross-checked against another source of
// pressure-altitude and verified as being consistent, or is based on a non-Gilham coded source
NICBGilhamCrossCheckedOrNonGilham
)
// ParseNICBaro reads the NICBaro from a 56 bits data field
func ParseNICBaro(data []byte) NICBaro {
return NICBaro((data[5] & 0x10) >> 4)
}

View File

@@ -0,0 +1,78 @@
package fields
import "errors"
// GillhamToAltitude convert an altitude given in Gillham bits to an altitude in feet.
func GillhamToAltitude(d1, d2, d4, a1, a2, a4, b1, b2, b4, c1, c2, c4 bool) (int32, error) {
fiveHundredBits := uint16(0)
if d1 {
fiveHundredBits |= 0x0100
}
if d2 {
fiveHundredBits |= 0x0080
}
if d4 {
fiveHundredBits |= 0x0040
}
if a1 {
fiveHundredBits |= 0x0020
}
if a2 {
fiveHundredBits |= 0x0010
}
if a4 {
fiveHundredBits |= 0x0008
}
if b1 {
fiveHundredBits |= 0x0004
}
if b2 {
fiveHundredBits |= 0x0002
}
if b4 {
fiveHundredBits |= 0x0001
}
oneHundredBits := uint16(0)
if c1 {
oneHundredBits |= 0x0004
}
if c2 {
oneHundredBits |= 0x0002
}
if c4 {
oneHundredBits |= 0x0001
}
oneHundred := int32(grayToBinary(oneHundredBits))
fiveHundred := int32(grayToBinary(fiveHundredBits))
// Check for invalid codes.
if oneHundred == 5 || oneHundred == 6 || oneHundred == 0 {
return 0, errors.New("the bits C1 to to C4 are incorrect")
}
// Remove 7s from OneHundreds.
if oneHundred == 7 {
oneHundred = 5
}
// Correct order of OneHundreds.
if fiveHundred%2 != 0 {
oneHundred = 6 - oneHundred
}
// Convert to feet and apply altitude datum offset.
return (int32(fiveHundred)*500 + int32(oneHundred)*100) - 1300, nil
}
func grayToBinary(num uint16) uint16 {
temp := uint16(0)
temp = num ^ (num >> 8)
temp ^= temp >> 4
temp ^= temp >> 2
temp ^= temp >> 1
return temp
}

View File

@@ -0,0 +1,52 @@
package fields
import "testing"
func TestGillhamToAltitude(t *testing.T) {
tests := []struct {
Test string
Want int32
}{
{"00000000010", -1000},
{"00000001010", -500},
{"00000011010", 0},
{"00000011110", 100},
{"00000010011", 600},
{"00000110010", 1000},
{"00001001001", 5800},
{"00011100100", 10300},
{"01100011010", 32000},
{"01110000100", 46300},
{"10000000011", 126600},
{"10000000001", 126700},
}
for _, test := range tests {
t.Run(test.Test, func(t *testing.T) {
alt, err := GillhamToAltitude(testString2Bits(test.Test))
if err != nil {
t.Fatalf("%v", err)
}
if alt != test.Want {
t.Errorf("expected altitude %d, got %d", test.Want, alt)
}
})
}
}
func testString2Bits(s string) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) {
d2 := s[0] == '1'
d4 := s[1] == '1'
a1 := s[2] == '1'
a2 := s[3] == '1'
a4 := s[4] == '1'
b1 := s[5] == '1'
b2 := s[6] == '1'
b4 := s[7] == '1'
c1 := s[8] == '1'
c2 := s[9] == '1'
c4 := s[10] == '1'
return false, d2, d4, a1, a2, a4, b1, b2, b4, c1, c2, c4
}

527
protocol/adsb/message.go Normal file
View File

@@ -0,0 +1,527 @@
package adsb
import (
"errors"
"fmt"
"io"
"git.maze.io/go/ham/protocol/adsb/fields"
)
var (
ErrLength = errors.New("adsb: the supplied data must be 7 bytes long")
ErrFormatType = errors.New("adsb: invalid format type code")
ErrSubType = errors.New("adsb: invalid sub type code")
)
type Payload interface {
UnmarshalBytes([]byte) error
}
type Message []byte
// FormatType returns the format type code
//
// Code BDS Description V0 V1 V2
//
// 0 ? No Position
// 1 0,8 Aircraft Id OK OK OK
// 2 0,8 Aircraft Id OK OK OK
// 3 0,8 Aircraft Id OK OK OK
// 4 0,8 Aircraft Id OK OK OK
// 5 0,6 Surface position OK OK OK
// 6 0,6 Surface position OK OK OK
// 7 0,6 Surface position OK OK OK
// 8 0,6 Surface position OK OK OK
// 9 0,5 Airborne position OK OK OK
//
// 10 0,5 Airborne position OK OK OK
// 14 0,5 Airborne position OK OK OK
// 12 0,5 Airborne position OK OK OK
// 13 0,5 Airborne position OK OK OK
// 14 0,5 Airborne position OK OK OK
// 15 0,5 Airborne position OK OK OK
// 16 0,5 Airborne position OK OK OK
// 17 0,5 Airborne position OK OK OK
// 18 0,5 Airborne position OK OK OK
// 19 0,9 Airborne velocity OK OK OK
// 20 0,5 Airborne position OK OK OK
// 21 0,5 Airborne position OK OK OK
// 22 0,5 Airborne position OK OK OK
// 23 Reserved
// 24 Reserved
// 25 Reserved
// 26 Reserved
// 27 Reserved
// 28 6,1 Emergency report OK OK OK
// 29 6,2 Target and status __ OK OK
// 30 Reserved
// 31 6,5 Operational status OK OK OK
func (msg Message) FormatType() byte {
if len(msg) < 7 {
return 0
}
return (msg[0] & 0xF8) >> 3
}
func (msg Message) Payload() (Payload, error) {
var payload Payload
switch formatType := msg.FormatType(); formatType {
case 0:
if msg[2]&0x0F == 0 && msg[3] == 0 && msg[4] == 0 && msg[5] == 0 && msg[6] == 0 {
payload = new(NoPositionInformation)
} else {
payload = new(AirbornePositionType0)
}
case 1, 2, 3, 4:
payload = new(AircraftIdentificationAndCategory)
case 5, 6, 7, 8:
payload = new(SurfacePosition)
case 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 21, 22:
payload = new(AirbornePosition)
case 19:
// check sub type
switch subType := msg[0] & 0x07; subType {
case 1:
payload = new(AirborneVelocityAirSpeedNormal)
case 2:
payload = new(AirborneVelocityAirSpeedSupersonic)
case 3:
payload = new(AirborneVelocityGroundSpeedNormal)
case 4:
payload = new(AirborneVelocityGroundSpeedSupersonic)
default:
return nil, fmt.Errorf("adsb: invalid airborne velocity sub type %d", subType)
}
case 28:
switch subType := msg[0] & 0x07; subType {
case 0:
payload = new(AircraftStatusNoInformation)
case 1:
payload = new(AircraftStatusEmergency)
case 2:
payload = new(AircraftStatusACAS)
default:
return nil, fmt.Errorf("adsb: invalid aircraft status sub type %d", subType)
}
case 29:
switch subType := (msg[0] & 0x06) >> 1; subType {
case 0:
payload = new(TargetStateAndStatus0)
case 1:
payload = new(TargetStateAndStatus1)
default:
return nil, fmt.Errorf("adsb: invalid target state and status sub type %d", subType)
}
case 31:
payload = new(AircraftOperationalStatus)
default:
return nil, fmt.Errorf("adsb: the format type code %d is not supported", formatType)
}
if err := payload.UnmarshalBytes(msg); err != nil {
return nil, err
}
return payload, nil
}
type NoPositionInformation struct {
FormatType byte
AltitudeBarometric int32
NavigationIntegrityCategory byte
}
func (msg *NoPositionInformation) UnmarshalBytes(data []byte) error {
if len(data) != 7 {
return ErrLength
}
// Update the first byte to have a standard BDS code 0,5 (Airborne position with baro altitude) - format 15
temp := []byte{0x78, data[1], data[2], 0, 0, 0, 0}
var pos AirbornePosition
if err := pos.UnmarshalBytes(temp); err != nil {
return err
}
msg.FormatType = pos.FormatType
msg.AltitudeBarometric = pos.AltitudeInFeet
msg.NavigationIntegrityCategory = 0
return nil
}
// AirbornePosition is a message at the format BDS 0,5
type AirbornePosition struct {
FormatType byte
SurveillanceStatus fields.SurveillanceStatus
HasDualAntenna bool
AltitudeSource fields.AltitudeSource
AltitudeReportMethod fields.AltitudeReportMethod
AltitudeInFeet int32
TimeSynchronizedToUTC bool
CompactPositionReportingFormat fields.CompactPositionReportingFormat
EncodedLatitude uint32
EncodedLongitude uint32
}
func (msg *AirbornePosition) UnmarshalBytes(data []byte) error {
if len(data) != 7 {
return ErrLength
}
msg.FormatType = Message(data).FormatType()
if msg.FormatType < 9 || msg.FormatType > 22 || msg.FormatType == 19 {
return ErrFormatType
}
var err error
if msg.AltitudeInFeet, msg.AltitudeSource, msg.AltitudeReportMethod, err = fields.ParseAltitude(data); err != nil {
return err
}
return nil
}
type AirbornePositionType0 struct {
FormatTypeCode byte
NavigationIntegrityCategory byte
AirbornePosition
}
func (msg *AirbornePositionType0) UnmarshalBytes(data []byte) error {
// Update the first byte to have a standard BDS code 0,5 (Airborne position with baro altitude) - format 22
temp := []byte{0xB0, data[1], data[2], data[3], data[4], data[5], data[6]}
if err := msg.AirbornePosition.UnmarshalBytes(temp); err != nil {
return err
}
msg.NavigationIntegrityCategory = 0
return nil
}
type AircraftIdentificationAndCategory struct {
FormatType byte
CategorySet byte
Category byte
Identification string
}
func (msg *AircraftIdentificationAndCategory) UnmarshalBytes(data []byte) error {
if len(data) < 7 {
return io.ErrUnexpectedEOF
}
msg.FormatType = Message(data).FormatType()
for msg.FormatType < 1 || msg.FormatType > 4 {
return ErrFormatType
}
msg.Category = data[0] & 0x07
switch msg.FormatType {
case 1:
msg.CategorySet = 'D'
case 2:
msg.CategorySet = 'C'
case 3:
msg.CategorySet = 'B'
default:
msg.CategorySet = 'A'
}
// Get the codes
codes := make([]byte, 8)
codes[0] = (data[1] & 0xFC) >> 2
codes[1] = (data[1]&0x03)<<4 + (data[2]&0xF0)>>4
codes[2] = (data[2]&0x0F)<<2 + (data[3]&0xC0)>>6
codes[3] = data[3] & 0x3F
codes[4] = (data[4] & 0xFC) >> 2
codes[5] = (data[4]&0x03)<<4 + (data[5]&0xF0)>>4
codes[6] = (data[5]&0x0F)<<2 + (data[6]&0xC0)>>6
codes[7] = data[6] & 0x3F
// Convert the codes to actual char
chars := make([]byte, 8)
for i, code := range codes {
chars[i] = identificationCharacterCoding[code]
}
msg.Identification = string(chars)
return nil
}
type SurfacePosition struct {
FormatType byte
MovementStatus fields.MovementStatus
MovementSpeed float64
GroundTrackStatus bool
GroundTrack float64
TimeSynchronizedToUTC bool
CompactPositionReportingFormat fields.CompactPositionReportingFormat
EncodedLatitude fields.EncodedLatitude
EncodedLongitude fields.EncodedLongitude
}
func (msg *SurfacePosition) UnmarshalBytes(data []byte) error {
if len(data) != 7 {
return ErrLength
}
msg.FormatType = (data[0] & 0xF8) >> 3
if msg.FormatType < 5 || msg.FormatType > 8 {
return ErrFormatType
}
msg.MovementSpeed, msg.MovementStatus = fields.ParseMovementStatus(data)
msg.GroundTrack, msg.GroundTrackStatus = fields.ParseGroundTrackStatus(data)
msg.TimeSynchronizedToUTC = fields.ParseTimeSynchronizedToUTC(data)
msg.CompactPositionReportingFormat = fields.ParseCompactPositioningReportFormat(data)
msg.EncodedLatitude = fields.ParseEncodedLatitude(data)
msg.EncodedLongitude = fields.ParseEncodedLongitude(data)
return nil
}
type AirborneVelocity struct {
FormatType byte
SubType byte
IntentChange bool
IFRCapability bool
NavigationUncertaintyCategory fields.NavigationUncertaintyCategory
MagneticHeadingStatus bool
MagneticHeading float64
AirspeedStatus fields.NumericValueStatus
Airspeed uint16
VerticalRateSource fields.VerticalRateSource
VerticalRateStatus fields.NumericValueStatus
VerticalRate int16
}
// AirborneVelocityGroundSpeedNormal is a message at the format BDS 9,0
type AirborneVelocityGroundSpeedNormal struct {
AirborneVelocity
DirectionEastWest fields.DirectionEastWest
VelocityEWStatus fields.NumericValueStatus
VelocityEW uint16
DirectionNorthSouth fields.DirectionNorthSouth
VelocityNSStatus fields.NumericValueStatus
VelocityNS uint16
DifferenceAltitudeGNSSBaroStatus fields.NumericValueStatus
DifferenceAltitudeGNSSBaro int16
}
func (msg *AirborneVelocityGroundSpeedNormal) UnmarshalBytes(data []byte) error {
if err := msg.unmarshalBytes(data, 1); err != nil {
return err
}
msg.Airspeed, msg.AirspeedStatus = fields.ParseAirspeedSupersonic(data)
msg.DirectionEastWest = fields.ParseDirectionEastWest(data)
msg.VelocityEW, msg.VelocityEWStatus = fields.ParseVelocityEastWestNormal(data)
msg.DirectionNorthSouth = fields.ParseDirectionNorthSouth(data)
msg.VelocityNS, msg.VelocityNSStatus = fields.ParseVelocityNorthSouthNormal(data)
msg.DifferenceAltitudeGNSSBaro, msg.DifferenceAltitudeGNSSBaroStatus = fields.ParseHeightDifferenceFromBaro(data)
return nil
}
// AirborneVelocityGroundSpeedSupersonic is a message at the format BDS 9,0
type AirborneVelocityGroundSpeedSupersonic struct {
AirborneVelocity
DirectionEastWest fields.DirectionEastWest
VelocityEWStatus fields.NumericValueStatus
VelocityEW uint16
DirectionNorthSouth fields.DirectionNorthSouth
VelocityNSStatus fields.NumericValueStatus
VelocityNS uint16
DifferenceAltitudeGNSSBaroStatus fields.NumericValueStatus
DifferenceAltitudeGNSSBaro int16
}
func (msg *AirborneVelocityGroundSpeedSupersonic) UnmarshalBytes(data []byte) error {
if err := msg.unmarshalBytes(data, 2); err != nil {
return err
}
msg.Airspeed, msg.AirspeedStatus = fields.ParseAirspeedSupersonic(data)
msg.DirectionEastWest = fields.ParseDirectionEastWest(data)
msg.VelocityEW, msg.VelocityEWStatus = fields.ParseVelocityEastWestSupersonic(data)
msg.DirectionNorthSouth = fields.ParseDirectionNorthSouth(data)
msg.VelocityNS, msg.VelocityNSStatus = fields.ParseVelocityNorthSouthSupersonic(data)
msg.DifferenceAltitudeGNSSBaro, msg.DifferenceAltitudeGNSSBaroStatus = fields.ParseHeightDifferenceFromBaro(data)
return nil
}
type AirborneVelocityAirSpeedNormal struct {
AirborneVelocity
HeightDifferenceFromBaroStatus fields.NumericValueStatus
HeightDifferenceFromBaro int16
}
func (msg *AirborneVelocity) unmarshalBytes(data []byte, subType byte) error {
if len(data) != 7 {
return ErrLength
}
msg.FormatType = (data[0] & 0xF8) >> 3
if msg.FormatType != 19 {
return ErrFormatType
}
msg.SubType = data[0] & 0x07
if msg.SubType != subType {
return ErrSubType
}
msg.IntentChange = fields.ParseIntentChange(data)
msg.IFRCapability = fields.ParseIFRCapability(data)
msg.NavigationUncertaintyCategory = fields.ParseeNavigationUncertaintyCategory(data)
msg.MagneticHeading, msg.MagneticHeadingStatus = fields.ParseMagneticHeading(data)
msg.VerticalRateSource = fields.ParseVerticalRateSource(data)
msg.VerticalRate, msg.VerticalRateStatus = fields.ParseVerticalRate(data)
return nil
}
func (msg *AirborneVelocityAirSpeedNormal) UnmarshalBytes(data []byte) error {
if err := msg.unmarshalBytes(data, 3); err != nil {
return err
}
msg.Airspeed, msg.AirspeedStatus = fields.ParseAirspeedNormal(data)
msg.HeightDifferenceFromBaro, msg.HeightDifferenceFromBaroStatus = fields.ParseHeightDifferenceFromBaro(data)
return nil
}
type AirborneVelocityAirSpeedSupersonic struct {
AirborneVelocity
HeightDifferenceFromBaroStatus fields.NumericValueStatus
HeightDifferenceFromBaro int16
}
func (msg *AirborneVelocityAirSpeedSupersonic) UnmarshalBytes(data []byte) error {
if err := msg.unmarshalBytes(data, 4); err != nil {
return err
}
msg.Airspeed, msg.AirspeedStatus = fields.ParseAirspeedSupersonic(data)
msg.HeightDifferenceFromBaro, msg.HeightDifferenceFromBaroStatus = fields.ParseHeightDifferenceFromBaro(data)
return nil
}
type AircraftStatusNoInformation struct {
FormatType byte
SubType byte
EmergencyPriorityStatus fields.EmergencyPriorityStatus
}
func (msg *AircraftStatusNoInformation) UnmarshalBytes(data []byte) error {
if len(data) != 7 {
return ErrLength
}
msg.FormatType = (data[0] & 0xF8) >> 3
if msg.FormatType != 28 {
return ErrFormatType
}
msg.SubType = data[0] & 0x07
if msg.SubType != 0 {
return ErrSubType
}
msg.EmergencyPriorityStatus = fields.ParseEmergencyPriorityStatus(data)
return nil
}
type AircraftStatusEmergency struct {
FormatType byte
SubType byte
EmergencyPriorityStatus fields.EmergencyPriorityStatus
ModeACode uint16
}
func (msg *AircraftStatusEmergency) UnmarshalBytes(data []byte) error {
if len(data) != 7 {
return ErrLength
}
msg.FormatType = (data[0] & 0xF8) >> 3
if msg.FormatType != 28 {
return ErrFormatType
}
msg.SubType = data[0] & 0x07
if msg.SubType != 1 {
return ErrSubType
}
msg.EmergencyPriorityStatus = fields.ParseEmergencyPriorityStatus(data)
msg.ModeACode = uint16((data[1]&0x1F)<<8) | uint16(data[2])
return nil
}
type AircraftStatusACAS struct {
FormatType byte
SubType byte
ResolutionAdvisory ResolutionAdvisory
}
type TargetStateAndStatus0 struct {
FormatType byte
SubType byte
VerticalDataAvailableSourceIndicator fields.VerticalDataAvailableSourceIndicator
TargetAltitudeType fields.TargetAltitudeType
TargetAltitudeCapability fields.TargetAltitudeCapability
VerticalModeIndicator fields.VerticalModeIndicator
TargetAltitudeStatus fields.NumericValueStatus
TargetAltitude int32
HorizontalDataAvailableSourceIndicator fields.HorizontalDataAvailableSourceIndicator
TargetHeadingTrackAngleStatus fields.NumericValueStatus
TargetHeadingTrackAngle uint16
TargetHeadingTrackIndicator fields.TargetHeadingTrackIndicator
HorizontalModeIndicator fields.HorizontalModeIndicator
NavigationalAccuracyCategoryPosition fields.NavigationalAccuracyCategoryPositionV1
NICBaro fields.NICBaro
SurveillanceIntegrityLevel fields.SurveillanceIntegrityLevel
CapabilityModeCode fields.CapabilityModeCode
EmergencyPriorityStatus fields.EmergencyPriorityStatus
}
func (msg *TargetStateAndStatus0) UnmarshalBytes(data []byte) error {
if len(data) != 7 {
return ErrLength
}
msg.FormatType = (data[0] & 0xF8) >> 3
if msg.FormatType != 29 {
return ErrFormatType
}
msg.SubType = (data[0] & 0x06) >> 1
if msg.SubType != 0 {
return ErrSubType
}
}
var identificationCharacterCoding = []byte{
'#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#', '#', '#', '#', '#',
' ', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#', '#',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '#', '#', '#', '#', '#',
}

137
protocol/adsb/ra.go Normal file
View File

@@ -0,0 +1,137 @@
package adsb
import (
"encoding/binary"
"errors"
)
// ResolutionAdvisory is an ACAS message providing information about ResolutionAdvisory
//
// Defined at 3.1.2.8.3.1 and 4.3.8.4.2.4
type ResolutionAdvisory struct {
ActiveRA ActiveResolutionAdvisory
RAComplement RAComplement
RATerminatedIndicator RATerminatedIndicator
MultipleThreatEncounter MultipleThreatEncounter
ThreatTypeIndicator ThreatTypeIndicator
ThreatIdentityAddress *ThreatIdentityAddress
ThreatIdentityAltitude *ThreatIdentityAltitude
ThreatIdentityRange *ThreatIdentityRange
ThreatIdentityBearing *ThreatIdentityBearing
}
// ThreatTypeIndicator indicates the type of information contained in the TID part of the message
type ThreatTypeIndicator int
// ThreatIdentityAddress is a 3 bytes ICAO Address. The Most Significant Byte of the address
// is always 0.
type ThreatIdentityAddress uint32
const (
ThreatTypeNoIdentity ThreatTypeIndicator = iota // No identity data in TID
ThreatTypeModeS // Contains a Mode S transponder address
ThreatTypeAltitudeRangeBearing // Contains altitude, range and bearing data
ThreatTypeReserved3 // Reserved
)
// ThreatIdentityAltitude is the altitude of the threat. It is given in 100 feet increment
type ThreatIdentityAltitude struct {
AltitudeValid bool
AltitudeInFeet int32
}
func parseThreatTypeIndicator(data []byte) ThreatTypeIndicator {
return ThreatTypeIndicator((data[2] & 0x0C) >> 2)
}
// parseThreatIdentityAltitude reads the altitude code from a message
func parseThreatIdentityAltitude(data []byte) (ThreatIdentityAltitude, error) {
// Altitude code is a 13 bits fields, so read a uint16
// byte data[2] | data[3] | data[4]
// bit 19 20 21 22 23|24 25 26 27 28 29 30 31|32 33 34 35 36
// value _ _ _ C1 A1|C2 A2 C4 A4 0 B1 D1 B2|D2 B4 D4 _ _
// Start by D2 B4 D4
altitudeCode := uint16(data[4]&0xE0) >> 5
// Then pack B1 D1 B2
altitudeCode += uint16(data[3]&0x07) << 3
// Then C2 A2 C4 A4
altitudeCode += uint16(data[3]&0xF0) << 2
// Then C1 A1
altitudeCode += uint16(data[2]&0x03) << 2
// Detect invalid altitude
if altitudeCode == 0 {
return ThreatIdentityAltitude{}, nil
}
c1 := (altitudeCode & 0x0800) != 0
a1 := (altitudeCode & 0x0400) != 0
c2 := (altitudeCode & 0x0200) != 0
a2 := (altitudeCode & 0x0100) != 0
c4 := (altitudeCode & 0x0080) != 0
a4 := (altitudeCode & 0x0040) != 0
b1 := (altitudeCode & 0x0020) != 0
d1 := (altitudeCode & 0x0010) != 0
b2 := (altitudeCode & 0x0008) != 0
d2 := (altitudeCode & 0x0004) != 0
b4 := (altitudeCode & 0x0002) != 0
d4 := (altitudeCode & 0x0001) != 0
altitudeFeet, err := gillhamToAltitude(d1, d2, d4, a1, a2, a4, b1, b2, b4, c1, c2, c4)
if err != nil {
return ThreatIdentityAltitude{}, errors.New("adsb: the altitude field is malformed")
}
return ThreatIdentityAltitude{true, altitudeFeet}, nil
}
// ThreatIdentityRange is TIDR (threat identity data range subfield). This 7-bit subfield (76-82) shall contain the
// most recent threat range estimated by ACAS.
type ThreatIdentityRange byte
func parseThreatIdentityRange(data []byte) ThreatIdentityRange {
return ThreatIdentityRange((data[4]&0x1F)<<2 + (data[5]&0xA0)>>6)
}
// ThreatIdentityBearing is TIDR (threat identity data bearing subfield). This 6-bit subfield (83-88) shall contain the
// most recent estimated bearing of the threat aircraft, relative to the ACAS aircraft heading.
type ThreatIdentityBearing byte
func parseThreatIdentityBearing(data []byte) ThreatIdentityBearing {
return ThreatIdentityBearing(data[5] & 0x3F)
}
func parseResolutionAdvisory(data []byte) (*ResolutionAdvisory, error) {
if len(data) != 6 {
return nil, errors.New("adsb: data for ACAS ResolutionAdvisory must be 6 bytes long")
}
// Format of the message is as follows:
// 0 1 2 3 4 5
// | RAC | R R M T TID | TID | TID | TID |
// ARA | ARA RAC | A A T T d d | d d d d d d d d | d d d d d d d d | d d d d d d _ _ |
// | RAC | C T E I TIDA| TIDA | TIDA TIDR |TIDR TIDB |
// a a a a a a a a | a a a a a a c c | c c t m i i a a | a a a a a a a a | a a a r r r r r | r r b b b b b b |
ra := new(ResolutionAdvisory)
switch ra.ThreatTypeIndicator = parseThreatTypeIndicator(data); ra.ThreatTypeIndicator {
case ThreatTypeModeS:
addr := ThreatIdentityAddress((binary.BigEndian.Uint32(data[2:]) >> 2) & 0xFFFFFF)
ra.ThreatIdentityAddress = &addr
case ThreatTypeAltitudeRangeBearing:
altitude, err := parseThreatIdentityAltitude(data)
if err != nil {
return nil, err
}
ra.ThreatIdentityAltitude = &altitude
threatRange := parseThreatIdentityRange(data)
ra.ThreatIdentityRange = &threatRange
bearing := parseThreatIdentityBearing(data)
ra.ThreatIdentityBearing = &bearing
}
}

View File

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

View File

@@ -0,0 +1,219 @@
package aprs
import (
"errors"
"fmt"
)
var (
ErrLatitude = errors.New("aprs: invalid latitude")
ErrLongitude = errors.New("aprs: invalid longitude")
)
// Latitude is the north-south position. Positive values are North, negative are South.
type Latitude float64
func LatitudeFromDMH(degrees, minutes, hundreths int, north bool) Latitude {
v := float64(degrees) + float64(minutes)/60 + float64(hundreths)/6000
for v > 90 {
v -= 180
}
for v < -90 {
v += 180
}
if north {
return Latitude(v)
}
return -Latitude(v)
}
func (lat Latitude) DMH() (degrees, minutes, hundreths int, north bool) {
degrees = int(lat)
minutes = int((float64(lat) - float64(degrees)) * 60)
hundreths = int((float64(lat) - float64(degrees) - float64(minutes)/60) * 6000)
if hundreths == 100 {
hundreths = 0
minutes += 1
}
if minutes == 60 {
minutes = 0
degrees += 1
}
north = lat >= 0
return
}
func (lat *Latitude) ParseCompressed(b []byte) error {
if len(b) != 4 {
return ErrLatitude
}
n, err := base91Decode(string(b))
if err != nil {
return err
}
*lat = Latitude(90 - float64(n)/380926)
return nil
}
func (lat *Latitude) ParseUncompressed(b []byte) error {
if len(b) != 8 || b[4] != '.' {
return ErrLatitude
}
var north bool
switch b[7] {
case 'N':
north = true
case 'S':
north = false
default:
return ErrLatitude
}
var (
degrees, minutes, hundreths int
err error
)
if degrees, err = parseBytesWithSpaces(b[0:2]); err != nil {
return err
}
if minutes, err = parseBytesWithSpaces(b[2:4]); err != nil {
return err
}
if hundreths, err = parseBytesWithSpaces(b[5:7]); err != nil {
return err
}
*lat = LatitudeFromDMH(degrees, minutes, hundreths, north)
return nil
}
func (lat Latitude) Compressed(b []byte) {
v := int((90 - float64(lat)) * 380926.0)
base91Encode(b, v)
}
func (lat Latitude) Uncompressed(b []byte) {
var (
degrees, minutes, hundreths, north = lat.DMH()
v = fmt.Sprintf("%02d%02d.%02d", degrees, minutes, hundreths)
)
if north {
v += "N"
} else {
v += "S"
}
copy(b, []byte(b))
}
// Longitude is the east-west position. Positive values are East, negative are West.
type Longitude float64
func LongitudeFromDMH(degrees, minutes, hundreths int, east bool) Longitude {
v := float64(degrees) + float64(minutes)/60 + float64(hundreths)/6000
for v > 180 {
v -= 360
}
for v < -180 {
v += 360
}
if east {
return Longitude(v)
}
return -Longitude(v)
}
func (long Longitude) DMH() (degrees, minutes, hundreths int, east bool) {
degrees = int(long)
minutes = int((float64(long) - float64(degrees)) * 60)
hundreths = int((float64(long) - float64(degrees) - float64(minutes)/60) * 6000)
if hundreths == 100 {
hundreths = 0
minutes += 1
}
if minutes == 60 {
minutes = 0
degrees += 1
}
east = long >= 0
return
}
func (long *Longitude) ParseCompressed(b []byte) error {
if len(b) != 4 {
return ErrLatitude
}
n, err := base91Decode(string(b))
if err != nil {
return err
}
*long = Longitude(float64(n)/190463.0 - 180)
return nil
}
func (long *Longitude) ParseUncompressed(b []byte) error {
if len(b) != 9 || b[5] != '.' {
return ErrLongitude
}
var east bool
switch b[8] {
case 'E':
east = true
case 'W':
east = false
default:
return ErrLongitude
}
var (
degrees, minutes, hundreths int
err error
)
if degrees, err = parseBytesWithSpaces(b[0:3]); err != nil {
return err
}
if minutes, err = parseBytesWithSpaces(b[3:5]); err != nil {
return err
}
if hundreths, err = parseBytesWithSpaces(b[6:8]); err != nil {
return err
}
*long = LongitudeFromDMH(degrees, minutes, hundreths, east)
return nil
}
func (long Longitude) Compressed(b []byte) {
v := int((180 + float64(long)) * 190463)
base91Encode(b, v)
}
func (long Longitude) Uncompressed(b []byte) {
var (
degrees, minutes, hundreths, east = long.DMH()
v = fmt.Sprintf("%03d%02d.%02d", degrees, minutes, hundreths)
)
if east {
v += "E"
} else {
v += "W"
}
copy(b, []byte(b))
}

View File

@@ -0,0 +1,53 @@
package aprs
import "testing"
func TestLatitude(t *testing.T) {
tests := []struct {
Test string
Want Latitude
}{
{"4903.50N", 49.05833333333333},
{"4903.50S", -49.05833333333333},
{"4903.5 S", -49.05833333333333},
{"4903. S", -49.05},
{"490 . S", -49},
{"4 . S", -40},
}
for _, test := range tests {
t.Run(test.Test, func(t *testing.T) {
var lat Latitude
if err := lat.ParseUncompressed([]byte(test.Test)); err != nil {
t.Fatal(err)
}
if !testAlmostEqual(float64(test.Want), float64(lat)) {
t.Errorf("expected %f, got %f", test.Want, lat)
}
})
}
}
func TestLongitude(t *testing.T) {
tests := []struct {
Test string
Want Longitude
}{
{"00000.00E", 0},
{"00000.00W", 0},
{"00000.98W", -0.016333},
{"00098. W", -1.633333},
{"098 . W", -98.000000},
{"9 . W", -180.000000},
}
for _, test := range tests {
t.Run(test.Test, func(t *testing.T) {
var long Longitude
if err := long.ParseUncompressed([]byte(test.Test)); err != nil {
t.Fatal(err)
}
if !testAlmostEqual(float64(test.Want), float64(long)) {
t.Errorf("expected %f, got %f", test.Want, long)
}
})
}
}

View File

@@ -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,
}
}

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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
}