Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/main/java/org/opensky/libadsb/PositionDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ else if (!msg.isOddFormat() && msg.hasPosition()) {
reasonable = false;
}
ret.setReasonable(reasonable);
last_pos = ret;
}
last_pos = ret;
last_time = time;

if (!reasonable)
Expand Down Expand Up @@ -424,9 +424,8 @@ else if (!msg.isOddFormat() && msg.hasPosition()) {
reasonable = false;
}
ret.setReasonable(reasonable);
last_pos = ret;
}

last_pos = ret;
last_time = time;

if (!reasonable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ public AirborneOperationalStatusV1Msg(ExtendedSquitter squitter) throws BadForma
subtype_code = (byte)(msg[0] & 0x7);
if (subtype_code > 1) // currently only 0 and 1 specified, 2-7 are reserved
throw new UnspecifiedFormatError("Operational status message subtype "+subtype_code+" reserved.");

capability_class_code = (msg[1]<<8)|msg[2];
if (subtype_code != 0) {
throw new BadFormatException("Not an airborne operational status message");
}
operational_mode_code = (msg[3]<<8)|msg[4];

capability_class_code = ((msg[1]&0xFF)<<8)|(msg[2]&0xFF);
operational_mode_code = ((msg[3]&0xFF)<<8)|(msg[4]&0xFF);
version = (byte) ((msg[5]>>>5) & 0x07);

if ((capability_class_code & 0xC000) != 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ private double NL(double Rlat) {
else if (Math.abs(Rlat) > 87) return 1;

double tmp = 1-(1-Math.cos(Math.PI/(2.0*15.0)))/Math.pow(Math.cos(Math.PI/180.0*Math.abs(Rlat)), 2);
if (tmp < -1) tmp = -1;
if (tmp > 1) tmp = 1;
return Math.floor(2*Math.PI/Math.acos(tmp));
}

Expand Down
12 changes: 7 additions & 5 deletions src/main/java/org/opensky/libadsb/msgs/AirspeedHeadingMsg.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public AirspeedHeadingMsg(ExtendedSquitter squitter) throws BadFormatException {

// heading available in ADS-B version 1+, indicates true/magnetic north for version 0
heading_status_bit = (msg[1]&0x4)>0;
heading = ((msg[1]&0x3)<<8 | msg[2]&0xFF) * 360/1024;
heading = ((msg[1]&0x3)<<8 | msg[2]&0xFF) * 360.0/1024.0;

true_airspeed = (msg[3]&0x80)>0;
airspeed = (short) (((msg[3]&0x7F)<<3 | msg[4]>>>5&0x07)-1);
Expand All @@ -103,11 +103,13 @@ public AirspeedHeadingMsg(ExtendedSquitter squitter) throws BadFormatException {

vertical_source = (msg[4]&0x10)>0;
vertical_rate_down = (msg[4]&0x08)>0;
vertical_rate = (short) ((((msg[4]&0x07)<<6 | msg[5]>>>2&0x3F)-1)<<6);
if (vertical_rate == -1) vertical_rate_info_available = false;
int raw_vr = ((msg[4]&0x07)<<6 | msg[5]>>>2&0x3F);
if (raw_vr == 0) vertical_rate_info_available = false;
else vertical_rate = (short) ((raw_vr-1)<<6);

geo_minus_baro = (short) (((msg[6]&0x7F)-1)*25);
if (geo_minus_baro == -1) geo_minus_baro_available = false;
int raw_gmb = msg[6]&0x7F;
if (raw_gmb == 0) geo_minus_baro_available = false;
else geo_minus_baro = (short) ((raw_gmb-1)*25);
if ((msg[6]&0x80)>0) geo_minus_baro *= -1;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public EmergencyOrPriorityStatusMsg(ExtendedSquitter squitter) throws BadFormatE
}

emergency_state = (byte) ((msg[1]&0xFF)>>>5);
mode_a_code = (short) (msg[2]|((msg[1]&0x1F)<<8));
mode_a_code = (short) ((msg[2]&0xFF)|((msg[1]&0x1F)<<8));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ public SurfaceOperationalStatusV1Msg(ExtendedSquitter squitter) throws BadFormat
if (subtype_code > 1) // currently only 0 and 1 specified, 2-7 are reserved
throw new UnspecifiedFormatError("Operational status message subtype "+subtype_code+" reserved.");

capability_class_code = (msg[1]<<4)|(msg[2]&0xF0)>>>4;
airplane_len_width = (byte) (msg[2]&0xF);
if (subtype_code != 1) {
throw new BadFormatException("Not surface operational status message");
}

operational_mode_code = (msg[3]<<8)|msg[4];
capability_class_code = ((msg[1]&0xFF)<<4)|((msg[2]&0xF0)>>>4);
airplane_len_width = (byte) (msg[2]&0xF);

operational_mode_code = ((msg[3]&0xFF)<<8)|(msg[4]&0xFF);
version = (byte) ((msg[5]>>>5) & 0x07);

if ((capability_class_code & 0xC000) != 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ private double NL(double Rlat) {
else if (Math.abs(Rlat) > 87) return 1;

double tmp = 1-(1-Math.cos(Math.PI/(2.0*15.0)))/Math.pow(Math.cos(Math.PI/180.0*Math.abs(Rlat)), 2);
if (tmp < -1) tmp = -1;
if (tmp > 1) tmp = 1;
return Math.floor(2*Math.PI/Math.acos(tmp));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ public TCASResolutionAdvisoryMsg(ExtendedSquitter squitter) throws BadFormatExce
if (msg_subtype != 2)
throw new BadFormatException("TCAS RA reports have subtype 2.");

active_ra = (short) (((msg[2]>>>2)&0x3f | (msg[1]<<6)) & 0x3FFF);
racs_record = (byte) ((((msg[2]&0x3)<<2) | (msg[3]>>>6)&0x3) & 0xF);
active_ra = (short) ((((msg[2]&0xFF)>>>2)&0x3f | ((msg[1]&0xFF)<<6)) & 0x3FFF);
racs_record = (byte) ((((msg[2]&0x3)<<2) | ((msg[3]&0xFF)>>>6)&0x3) & 0xF);
ra_terminated = (msg[3]&0x20) > 0;
multi_threat_encounter = (msg[3]&0x10) > 0;
threat_type = (byte) ((msg[3]>>>2)&0x3);
threat_identity = (msg[6] | (msg[5]<<8) | (msg[4]<<16) | ((msg[4]&0x3)<<24)) & 0x3FFFFFF;
threat_identity = ((msg[6]&0xFF) | ((msg[5]&0xFF)<<8) | ((msg[4]&0xFF)<<16) | ((msg[3]&0x3)<<24)) & 0x3FFFFFF;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ public VelocityOverGroundMsg(ExtendedSquitter squitter) throws BadFormatExceptio

vertical_source = (msg[4]&0x10)>0;
vertical_rate_down = (msg[4]&0x08)>0;
vertical_rate = (short) ((((msg[4]&0x07)<<6 | msg[5]>>>2&0x3F)-1)<<6);
if (vertical_rate == -1) vertical_rate_info_available = false;
int raw_vr = ((msg[4]&0x07)<<6 | msg[5]>>>2&0x3F);
if (raw_vr == 0) vertical_rate_info_available = false;
else vertical_rate = (short) ((raw_vr-1)<<6);

geo_minus_baro = msg[6]&0x7F;
if (geo_minus_baro == 0) geo_minus_baro_available = false;
Expand Down
118 changes: 118 additions & 0 deletions src/test/java/org/opensky/libadsb/PositionDecoderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package org.opensky.libadsb;

import org.junit.Test;
import org.opensky.libadsb.msgs.*;

import static org.junit.Assert.*;

/**
* Tests for the stateful PositionDecoder: CPR global/local decode, speed thresholds.
*
* Bug #9: After a failed decode (e.g., PositionStraddleError in global decode),
* last_pos is set to null, preventing subsequent local decodes from working.
* The fix: don't reset last_pos on decode failure.
*/
public class PositionDecoderTest {

@Test
public void testStatefulGlobalDecode() throws Exception {
PositionDecoder decoder = new PositionDecoder();

// Feed even frame first
AirbornePositionV0Msg even = (AirbornePositionV0Msg) Decoder.genericDecoder("8D40058B58C901375147EFD09357");
Position pos1 = decoder.decodePosition(0.0, even);
// First message alone may or may not produce a position

// Feed odd frame
AirbornePositionV0Msg odd = (AirbornePositionV0Msg) Decoder.genericDecoder("8D40058B58C904A87F402D3B8C59");
Position pos2 = decoder.decodePosition(1.0, odd);

// After receiving both even and odd frames, should have a valid position
assertNotNull("Should decode position after receiving even+odd pair", pos2);
assertEquals(49.82, pos2.getLatitude(), 0.02);
assertEquals(6.08, pos2.getLongitude(), 0.02);
}

@Test
public void testStatefulLocalDecodeAfterGlobal() throws Exception {
PositionDecoder decoder = new PositionDecoder();

// Establish position with even+odd pair
AirbornePositionV0Msg even = (AirbornePositionV0Msg) Decoder.genericDecoder("8D40058B58C901375147EFD09357");
decoder.decodePosition(0.0, even);

AirbornePositionV0Msg odd = (AirbornePositionV0Msg) Decoder.genericDecoder("8D40058B58C904A87F402D3B8C59");
Position globalPos = decoder.decodePosition(1.0, odd);
assertNotNull(globalPos);

// Feed another even frame - should use local decode
Position localPos = decoder.decodePosition(2.0, even);
assertNotNull("Local decode should succeed after global decode established position", localPos);
// Position should be close to the global decode result
assertEquals(globalPos.getLatitude(), localPos.getLatitude(), 0.05);
assertEquals(globalPos.getLongitude(), localPos.getLongitude(), 0.05);
}

@Test
public void testDecodeWithReceiverPosition() throws Exception {
PositionDecoder decoder = new PositionDecoder();
Position receiver = new Position(6.0, 49.0, 0.0);

AirbornePositionV0Msg even = (AirbornePositionV0Msg) Decoder.genericDecoder("8D40058B58C901375147EFD09357");
decoder.decodePosition(0.0, receiver, even);

AirbornePositionV0Msg odd = (AirbornePositionV0Msg) Decoder.genericDecoder("8D40058B58C904A87F402D3B8C59");
Position pos = decoder.decodePosition(1.0, receiver, odd);

assertNotNull(pos);
assertEquals(49.82, pos.getLatitude(), 0.02);
}

@Test
public void testSpeedThreshold() {
// withinThreshold checks if distance is realistic for time difference
// At aircraft speed (~250 m/s = 900 km/h), 10 seconds = 2500m max

// 1000m in 10s = 100 m/s - should be within threshold for airborne
assertTrue(PositionDecoder.withinThreshold(10.0, 1000.0, false));

// 100000m in 10s = 10000 m/s - way too fast
assertFalse(PositionDecoder.withinThreshold(10.0, 100000.0, false));
}

@Test
public void testWithinReasonableRange() {
// Receiver at (0, 0), sender at (6, 49) - well within 700km
Position receiver = new Position(0.0, 0.0, 0.0);
Position sender = new Position(6.0, 49.0, 39000.0);
// This is about 5500 km - should be OUT of range
assertFalse(PositionDecoder.withinReasonableRange(receiver, sender));

// Receiver at (6, 49), sender at (6.08, 49.82) - very close
Position nearReceiver = new Position(6.0, 49.0, 0.0);
Position nearSender = new Position(6.08, 49.82, 39000.0);
assertTrue(PositionDecoder.withinReasonableRange(nearReceiver, nearSender));
}

@Test
public void testSecondPairGlobalDecode() throws Exception {
// Test the second CPR pair using direct global decode
// pyModeS: lat=42.34736, lon=0.434982
AirbornePositionV0Msg odd = (AirbornePositionV0Msg) Decoder.genericDecoder("8d4d224f58bf07c2d41a9a353d70");
AirbornePositionV0Msg even = (AirbornePositionV0Msg) Decoder.genericDecoder("8d4d224f58bf003b221b34aa5b8d");
assertTrue(odd.isOddFormat());
assertFalse(even.isOddFormat());

// Use direct global decode instead of stateful decoder
try {
Position pos = even.getGlobalPosition(odd);
assertNotNull("Global position decode should succeed", pos);
assertEquals(42.35, pos.getLatitude(), 0.02);
assertEquals(0.43, pos.getLongitude(), 0.06);
} catch (Exception e) {
// May fail due to Bug #4 (NL function) or Bug #5 (longitude normalization)
// This is an expected failure until those bugs are fixed
fail("Global decode failed (may be Bug #4/#5): " + e.getMessage());
}
}
}
65 changes: 65 additions & 0 deletions src/test/java/org/opensky/libadsb/PositionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.opensky.libadsb;

import org.junit.Test;
import static org.junit.Assert.*;

/**
* Tests for the Position class: coordinates, haversine, ECEF conversion.
*/
public class PositionTest {

@Test
public void testConstructorAndGetters() {
Position pos = new Position(6.08, 49.82, 39000.0);
assertEquals(6.08, pos.getLongitude(), 0.001);
assertEquals(49.82, pos.getLatitude(), 0.001);
assertEquals(39000.0, pos.getAltitude(), 0.1);
}

@Test
public void testNullComponents() {
Position pos = new Position(null, null, null);
assertNull(pos.getLatitude());
assertNull(pos.getLongitude());
assertNull(pos.getAltitude());
}

@Test
public void testHaversineDistance() {
// Two known positions from CPR decode:
// pos1: 49.824097, 6.067850
// pos2: 49.817551, 6.084422
Position pos1 = new Position(6.067850, 49.824097, null);
Position pos2 = new Position(6.084422, 49.817551, null);
Double dist = pos1.haversine(pos2);
assertNotNull(dist);
// Distance should be roughly 1.3 km
assertTrue("Haversine distance should be > 500m", dist > 500);
assertTrue("Haversine distance should be < 3000m", dist < 3000);
}

@Test
public void testHaversineZeroDistance() {
Position pos = new Position(0.0, 0.0, null);
assertEquals(0.0, pos.haversine(pos), 0.001);
}

@Test
public void testHaversineAntipodal() {
// North pole to south pole: ~20,000 km
Position north = new Position(0.0, 90.0, null);
Position south = new Position(0.0, -90.0, null);
Double dist = north.haversine(south);
assertNotNull(dist);
assertEquals(20015000, dist, 100000); // ~20,015 km ± 100 km
}

@Test
public void testReasonableFlag() {
Position pos = new Position(6.0, 49.0, 39000.0);
// Default should be true
assertTrue(pos.isReasonable());
pos.setReasonable(false);
assertFalse(pos.isReasonable());
}
}
72 changes: 72 additions & 0 deletions src/test/java/org/opensky/libadsb/ToolsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.opensky.libadsb;

import org.junit.Test;
import static org.junit.Assert.*;

/**
* Tests for utility functions in the tools class: hex conversion, CRC, unit conversions.
* Expected values cross-validated with pyModeS 2.21.1 and lib1090.
*/
public class ToolsTest {

@Test
public void testHexStringToByteArray() {
byte[] result = tools.hexStringToByteArray("8D406B902015A678D4D220AA4BDA");
assertEquals(14, result.length);
assertEquals((byte) 0x8D, result[0]);
assertEquals((byte) 0x40, result[1]);
assertEquals((byte) 0x6B, result[2]);
assertEquals((byte) 0x90, result[3]);
assertEquals((byte) 0xDA, result[13]);
}

@Test
public void testHexStringToByteArrayLowerCase() {
byte[] upper = tools.hexStringToByteArray("8D406B90");
byte[] lower = tools.hexStringToByteArray("8d406b90");
assertTrue(tools.areEqual(upper, lower));
}

@Test
public void testToHexString() {
byte[] bytes = {(byte) 0x40, (byte) 0x6B, (byte) 0x90};
assertEquals("406b90", tools.toHexString(bytes));
}

@Test
public void testToHexStringSingleByte() {
assertEquals("0a", tools.toHexString((byte) 0x0A));
assertEquals("ff", tools.toHexString((byte) 0xFF));
assertEquals("00", tools.toHexString((byte) 0x00));
}

@Test
public void testIsZero() {
assertTrue(tools.isZero(new byte[]{0, 0, 0}));
assertFalse(tools.isZero(new byte[]{0, 0, 1}));
assertFalse(tools.isZero(new byte[]{(byte) 0xFF, 0, 0}));
}

@Test
public void testAreEqual() {
byte[] a = {1, 2, 3};
byte[] b = {1, 2, 3};
byte[] c = {1, 2, 4};
assertTrue(tools.areEqual(a, b));
assertFalse(tools.areEqual(a, c));
}

@Test
public void testFeet2Meters() {
assertEquals(0.0, tools.feet2Meters(0), 0.001);
assertEquals(304.8, tools.feet2Meters(1000), 0.1);
assertNull(tools.feet2Meters((Integer) null));
}

@Test
public void testKnots2MetersPerSecond() {
// 1 knot = 0.514444 m/s
assertEquals(0.514444, tools.knots2MetersPerSecond(1), 0.001);
assertNull(tools.knots2MetersPerSecond((Integer) null));
}
}
Loading