Fixed code smells
Some checks failed
Run tests / test (1.25) (push) Failing after 15s
Run tests / test (stable) (push) Failing after 17s

This commit is contained in:
2026-02-22 21:14:58 +01:00
parent 3bcbaf2135
commit 32f6c38c13
17 changed files with 1593 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

@@ -2,7 +2,6 @@ package aprs
import ( import (
"fmt" "fmt"
"strings"
) )
func base91Decode(s string) (n int, err error) { func base91Decode(s string) (n int, err error) {
@@ -18,6 +17,7 @@ func base91Decode(s string) (n int, err error) {
return return
} }
/*
func base91Encode(n int) string { func base91Encode(n int) string {
var s []string var s []string
for n > 0 { for n > 0 {
@@ -27,3 +27,4 @@ func base91Encode(n int) string {
} }
return strings.Join(s, "") return strings.Join(s, "")
} }
*/

View File

@@ -310,7 +310,7 @@ func (p *Packet) parse() error {
return err return err
} }
p.Position = &pos p.Position = &pos
p.parseMicEData() _ = p.parseMicEData()
return nil // there is no additional data to parse return nil // there is no additional data to parse
default: default:

View File

@@ -53,7 +53,7 @@ func (priv *PrivateKey) Sign(_ io.Reader, digest []byte, _ crypto.SignerOpts) (s
func (priv *PrivateKey) Public() crypto.PublicKey { func (priv *PrivateKey) Public() crypto.PublicKey {
p := &PublicKey{} p := &PublicKey{}
newPublicKey(p, priv.pub[:]) _, _ = newPublicKey(p, priv.pub[:])
return p return p
} }
@@ -118,7 +118,9 @@ func GenerateKey() (*PublicKey, *PrivateKey, error) {
} }
func generateKey(priv *PrivateKey) (*PrivateKey, error) { func generateKey(priv *PrivateKey) (*PrivateKey, error) {
rand.Read(priv.seed[:]) if _, err := rand.Read(priv.seed[:]); err != nil {
return nil, err
}
precomputePrivateKey(priv) precomputePrivateKey(priv)
return priv, nil return priv, nil
} }

View File

@@ -357,15 +357,6 @@ func (drv *companionDriver) wait(expect ...byte) ([]byte, error) {
return wait.Wait() return wait.Wait()
} }
func bytesContains(b byte, slice []byte) bool {
for _, v := range slice {
if v == b {
return true
}
}
return false
}
func (drv *companionDriver) handlePushFrame(b []byte) { func (drv *companionDriver) handlePushFrame(b []byte) {
if len(b) < 1 { if len(b) < 1 {
return // illegal return // illegal

View File

@@ -10,10 +10,7 @@ import (
"git.maze.io/go/ham/protocol/meshcore/crypto" "git.maze.io/go/ham/protocol/meshcore/crypto"
) )
var ( var zeroPositionBytes = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
zeroTime time.Time
zeroPositionBytes = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
)
type Payload interface { type Payload interface {
fmt.Stringer fmt.Stringer

View File

@@ -47,10 +47,6 @@ func decodeTime(b []byte) time.Time {
return time.Unix(int64(binary.LittleEndian.Uint32(b)), 0).UTC() return time.Unix(int64(binary.LittleEndian.Uint32(b)), 0).UTC()
} }
func encodeFrequency(b []byte, f float64) {
binary.LittleEndian.PutUint32(b, uint32(f*1e3))
}
func decodeFrequency(b []byte) float64 { func decodeFrequency(b []byte) float64 {
return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3 return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3
} }

173
protocol/meshtastic/node.go Normal file
View File

@@ -0,0 +1,173 @@
package meshtastic
import (
"crypto/rand"
"encoding/binary"
"fmt"
"math"
"math/big"
"strconv"
"strings"
)
type NodeID uint32
const (
// BroadcastNodeID is the special NodeID used when broadcasting a packet to a channel.
BroadcastNodeID NodeID = math.MaxUint32
// BroadcastNodeIDNoLora is a special broadcast address that excludes LoRa transmission.
// Used for MQTT-only broadcasts. This is ^all with the NO_LORA flag (0x40) cleared.
BroadcastNodeIDNoLora NodeID = math.MaxUint32 ^ 0x40
// ReservedNodeIDThreshold is the threshold at which NodeIDs are considered reserved. Random NodeIDs should not
// be generated below this threshold.
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L461
reservedNodeIDThreshold NodeID = 4
)
// ParseNodeID parses a NodeID from various string formats:
// - "!abcd1234" (Meshtastic format with ! prefix)
// - "0xabcd1234" (hex with 0x prefix)
// - "abcd1234" (plain hex)
// - "12345678" (decimal)
func ParseNodeID(s string) (NodeID, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("empty node ID string")
}
// Handle !prefix format
if strings.HasPrefix(s, "!") {
s = s[1:]
n, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
// Handle 0x prefix
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
n, err := strconv.ParseUint(s[2:], 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
// Try hex first if it looks like hex (contains a-f)
sLower := strings.ToLower(s)
if strings.ContainsAny(sLower, "abcdef") {
n, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
// Try decimal
n, err := strconv.ParseUint(s, 10, 32)
if err != nil {
// Fall back to hex for 8-char strings
if len(s) == 8 {
n, err = strconv.ParseUint(s, 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
return 0, fmt.Errorf("invalid node ID %q: %w", s, err)
}
return NodeID(n), nil
}
// Uint32 returns the underlying uint32 value of the NodeID.
func (n NodeID) Uint32() uint32 {
return uint32(n)
}
// String converts the NodeID to a hex formatted string.
// This is typically how NodeIDs are displayed in Meshtastic UIs.
func (n NodeID) String() string {
return fmt.Sprintf("!%08x", uint32(n))
}
// Bytes converts the NodeID to a byte slice
func (n NodeID) Bytes() []byte {
bytes := make([]byte, 4) // uint32 is 4 bytes
binary.BigEndian.PutUint32(bytes, n.Uint32())
return bytes
}
// DefaultLongName returns the default long node name based on the NodeID.
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L382
func (n NodeID) DefaultLongName() string {
bytes := make([]byte, 4) // uint32 is 4 bytes
binary.BigEndian.PutUint32(bytes, n.Uint32())
return fmt.Sprintf("Meshtastic %04x", bytes[2:])
}
// DefaultShortName returns the default short node name based on the NodeID.
// Last two bytes of the NodeID represented in hex.
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L382
func (n NodeID) DefaultShortName() string {
bytes := make([]byte, 4) // uint32 is 4 bytes
binary.BigEndian.PutUint32(bytes, n.Uint32())
return fmt.Sprintf("%04x", bytes[2:])
}
// UnmarshalText implements encoding.TextUnmarshaler for use with config parsers like Viper.
func (n *NodeID) UnmarshalText(text []byte) error {
parsed, err := ParseNodeID(string(text))
if err != nil {
return err
}
*n = parsed
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (n NodeID) MarshalText() ([]byte, error) {
return []byte(n.String()), nil
}
// IsReservedID returns true if this is a reserved or broadcast NodeID.
func (n NodeID) IsReservedID() bool {
return n < reservedNodeIDThreshold || n >= BroadcastNodeIDNoLora
}
// IsBroadcast returns true if this is any form of broadcast address.
func (n NodeID) IsBroadcast() bool {
return n == BroadcastNodeID || n == BroadcastNodeIDNoLora
}
// ToMacAddress returns a MAC address string derived from the NodeID.
// This creates a locally administered unicast MAC address.
func (n NodeID) ToMacAddress() string {
bytes := n.Bytes()
// Use 0x02 as the first octet (locally administered, unicast)
// Then 0x00 as padding, followed by the 4 bytes of the NodeID
return fmt.Sprintf("02:00:%02x:%02x:%02x:%02x", bytes[0], bytes[1], bytes[2], bytes[3])
}
// RandomNodeID returns a randomised NodeID.
// It's recommended to call this the first time a node is started and persist the result.
//
// Hardware meshtastic nodes first try a NodeID of the last four bytes of the BLE MAC address. If that ID is already in
// use or invalid, a random NodeID is generated.
// Source: https://github.com/meshtastic/firmware/blob/d1ea58975755e146457a8345065e4ca357555275/src/mesh/NodeDB.cpp#L466
func RandomNodeID() (NodeID, error) {
// Generates a random uint32 between reservedNodeIDThreshold and math.MaxUint32
randomInt, err := rand.Int(
rand.Reader,
big.NewInt(
int64(math.MaxUint32-reservedNodeIDThreshold.Uint32()),
),
)
if err != nil {
return NodeID(0), fmt.Errorf("reading entropy: %w", err)
}
r := uint32(randomInt.Uint64()) + reservedNodeIDThreshold.Uint32()
return NodeID(r), nil
}

View File

@@ -0,0 +1,54 @@
package meshtastic
import (
"encoding/binary"
"errors"
)
const (
minPacketSize = 4 + 4 + 4 + 1 + 1 + 1 + 1
maxPayloadSize = 237
)
var (
// ErrInvalidPacket signals the source buffer does not contain a valid packet.
ErrInvalidPacket = errors.New("meshtastic: invalid packet")
)
type Packet struct {
Destination NodeID
Source NodeID
ID uint32
Flags uint8
ChannelHash uint8
NextHop uint8
RelayNode uint8
PayloadLength int
Payload [maxPayloadSize]byte
}
func (packet *Packet) Decode(data []byte) error {
if len(data) < minPacketSize {
return ErrInvalidPacket
}
packet.Destination = parseNodeID(data[0:])
packet.Source = parseNodeID(data[4:])
packet.ID = binary.LittleEndian.Uint32(data[8:])
packet.Flags = data[12]
packet.ChannelHash = data[13]
packet.NextHop = data[14]
packet.RelayNode = data[15]
packet.PayloadLength = len(data[16:])
copy(packet.Payload[:], data[16:])
return nil
}
func (packet *Packet) HopLimit() int {
return
}
func parseNodeID(data []byte) NodeID {
return NodeID(binary.LittleEndian.Uint32(data))
}

View File

@@ -88,7 +88,7 @@ var parseLocatorMult = []struct {
{upper[:18], lower[:18], 20.0}, {upper[:18], lower[:18], 20.0},
{upper[:18], lower[:18], 10.0}, {upper[:18], lower[:18], 10.0},
{digit[:10], digit[:10], 20.0 / 10.0}, {digit[:10], digit[:10], 20.0 / 10.0},
{digit[:10], digit[:10], 10.0 / 10.0}, {digit[:10], digit[:10], 1.0},
{upper[:24], lower[:24], 20.0 / (10.0 * 24.0)}, {upper[:24], lower[:24], 20.0 / (10.0 * 24.0)},
{upper[:24], lower[:24], 10.0 / (10.0 * 24.0)}, {upper[:24], lower[:24], 10.0 / (10.0 * 24.0)},
{digit[:10], digit[:10], 20.0 / (10.0 * 24.0 * 10.0)}, {digit[:10], digit[:10], 20.0 / (10.0 * 24.0 * 10.0)},