1 /* 2 * Copyright (C) 2017 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.location.cts; 18 19 import android.location.cts.pseudorange.PseudorangePositionVelocityFromRealTimeEvents; 20 import android.location.GnssMeasurement; 21 import android.location.GnssMeasurementsEvent; 22 import android.location.GnssStatus; 23 import android.location.Location; 24 import android.platform.test.annotations.AppModeFull; 25 import android.util.Log; 26 import com.android.compatibility.common.util.CddTest; 27 import java.util.ArrayList; 28 import java.util.Collection; 29 import java.util.List; 30 import java.util.concurrent.TimeUnit; 31 import java.util.HashMap; 32 33 /** 34 * Test computing and verifying the pseudoranges based on the raw measurements 35 * reported by the GNSS chipset 36 */ 37 public class GnssPseudorangeVerificationTest extends GnssTestCase { 38 private static final String TAG = "GnssPseudorangeValTest"; 39 private static final int LOCATION_TO_COLLECT_COUNT = 5; 40 private static final int MEASUREMENT_EVENTS_TO_COLLECT_COUNT = 10; 41 private static final int MIN_SATELLITES_REQUIREMENT = 4; 42 private static final double SECONDS_PER_NANO = 1.0e-9; 43 44 // GPS/GLONASS: according to http://cdn.intechopen.com/pdfs-wm/27712.pdf, the pseudorange in 45 // time 46 // is 65-83 ms, which is 18 ms range. 47 // GLONASS: orbit is a bit closer than GPS, so we add 0.003ms to the range, hence deltaiSeconds 48 // should be in the range of [0.0, 0.021] seconds. 49 // QZSS and BEIDOU: they have higher orbit, which will result in a small svTime, the deltai 50 // can be 51 // calculated as follows: 52 // assume a = QZSS/BEIDOU orbit Semi-Major Axis(42,164km for QZSS); 53 // b = GLONASS orbit Semi-Major Axis (25,508km); 54 // c = Speed of light (299,792km/s); 55 // e = earth radius (6,378km); 56 // in the extremely case of QZSS is on the horizon and GLONASS is on the 90 degree top 57 // max difference should be (sqrt(a^2-e^2) - (b-e))/c, 58 // which is around 0.076s. 59 // 2 Galileo satellites (E14 & E18) have elliptical orbits, so Galileo can have up-to 48ms of 60 // spread. 61 private static final double PSEUDORANGE_THRESHOLD_IN_SEC = 0.048; 62 // Geosync constellations have a longer range vs typical MEO orbits 63 // that are the short end of the range. 64 private static final double PSEUDORANGE_THRESHOLD_BEIDOU_QZSS_IN_SEC = 0.076; 65 66 private static final float LOW_ENOUGH_POSITION_UNCERTAINTY_METERS = 100; 67 private static final float LOW_ENOUGH_VELOCITY_UNCERTAINTY_MPS = 5; 68 private static final float HORIZONTAL_OFFSET_FLOOR_METERS = 10; 69 private static final float HORIZONTAL_OFFSET_SIGMA = 3; // 3 * the ~68%ile level 70 private static final float HORIZONTAL_OFFSET_FLOOR_MPS = 1; 71 72 private TestGnssMeasurementListener mMeasurementListener; 73 private TestLocationListener mLocationListener; 74 75 @Override 76 protected void setUp() throws Exception { 77 super.setUp(); 78 79 mTestLocationManager = new TestLocationManager(getContext()); 80 } 81 82 @Override 83 protected void tearDown() throws Exception { 84 // Unregister listeners 85 if (mLocationListener != null) { 86 mTestLocationManager.removeLocationUpdates(mLocationListener); 87 } 88 if (mMeasurementListener != null) { 89 mTestLocationManager.unregisterGnssMeasurementCallback(mMeasurementListener); 90 } 91 super.tearDown(); 92 } 93 94 /** 95 * Tests that one can listen for {@link GnssMeasurementsEvent} for collection purposes. 96 * It only performs sanity checks for the measurements received. 97 * This tests uses actual data retrieved from Gnss HAL. 98 */ 99 @CddTest(requirement="7.3.3") 100 public void testPseudorangeValue() throws Exception { 101 // Checks if Gnss hardware feature is present, skips test (pass) if not, 102 // and hard asserts that Location/Gnss (Provider) is turned on if is Cts Verifier. 103 // From android O, CTS tests should run in the lab with GPS signal. 104 if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, true)) { 105 return; 106 } 107 108 mLocationListener = new TestLocationListener(LOCATION_TO_COLLECT_COUNT); 109 mTestLocationManager.requestLocationUpdates(mLocationListener); 110 111 mMeasurementListener = new TestGnssMeasurementListener(TAG, 112 MEASUREMENT_EVENTS_TO_COLLECT_COUNT, true); 113 mTestLocationManager.registerGnssMeasurementCallback(mMeasurementListener); 114 115 boolean success = mLocationListener.await(); 116 success &= mMeasurementListener.await(); 117 SoftAssert softAssert = new SoftAssert(TAG); 118 softAssert.assertTrue( 119 "Time elapsed without getting enough location fixes." 120 + " Possibly, the test has been run deep indoors." 121 + " Consider retrying test outdoors.", 122 success); 123 124 Log.i(TAG, "Location status received = " + mLocationListener.isLocationReceived()); 125 126 if (!mMeasurementListener.verifyStatus()) { 127 // If verifyStatus returns false, an assert exception happens and test fails. 128 return; // exit (with pass) 129 } 130 131 List<GnssMeasurementsEvent> events = mMeasurementListener.getEvents(); 132 int eventCount = events.size(); 133 Log.i(TAG, "Number of GNSS measurement events received = " + eventCount); 134 softAssert.assertTrue( 135 "GnssMeasurementEvent count: expected > 0, received = " + eventCount, 136 eventCount > 0); 137 138 boolean hasEventWithEnoughMeasurements = false; 139 // we received events, so perform a quick sanity check on mandatory fields 140 for (GnssMeasurementsEvent event : events) { 141 // Verify Gnss Event mandatory fields are in required ranges 142 assertNotNull("GnssMeasurementEvent cannot be null.", event); 143 144 long timeInNs = event.getClock().getTimeNanos(); 145 TestMeasurementUtil.assertGnssClockFields(event.getClock(), softAssert, timeInNs); 146 147 ArrayList<GnssMeasurement> filteredMeasurements = filterMeasurements(event.getMeasurements()); 148 HashMap<Integer, ArrayList<GnssMeasurement>> measurementConstellationMap = 149 groupByConstellation(filteredMeasurements); 150 for (ArrayList<GnssMeasurement> measurements : measurementConstellationMap.values()) { 151 validatePseudorange(measurements, softAssert, timeInNs); 152 } 153 154 // we need at least 4 satellites to calculate the pseudorange 155 if(event.getMeasurements().size() >= MIN_SATELLITES_REQUIREMENT) { 156 hasEventWithEnoughMeasurements = true; 157 } 158 } 159 160 softAssert.assertTrue( 161 "Should have at least one GnssMeasurementEvent with at least 4" 162 + "GnssMeasurement. If failed, retry near window or outdoors?", 163 hasEventWithEnoughMeasurements); 164 165 softAssert.assertAll(); 166 } 167 168 private HashMap<Integer, ArrayList<GnssMeasurement>> groupByConstellation( 169 Collection<GnssMeasurement> measurements) { 170 HashMap<Integer, ArrayList<GnssMeasurement>> measurementConstellationMap = new HashMap<>(); 171 for (GnssMeasurement measurement: measurements){ 172 int constellationType = measurement.getConstellationType(); 173 if (!measurementConstellationMap.containsKey(constellationType)) { 174 measurementConstellationMap.put(constellationType, new ArrayList<>()); 175 } 176 measurementConstellationMap.get(constellationType).add(measurement); 177 } 178 return measurementConstellationMap; 179 } 180 181 private static ArrayList<GnssMeasurement> filterMeasurements( 182 Collection<GnssMeasurement> measurements) { 183 ArrayList<GnssMeasurement> filteredMeasurement = new ArrayList<>(); 184 for (GnssMeasurement measurement : measurements) { 185 int constellationType = measurement.getConstellationType(); 186 if ((measurement.getState() & GnssMeasurement.STATE_CODE_LOCK) == 0) { 187 continue; 188 } 189 if (constellationType == GnssStatus.CONSTELLATION_GLONASS) { 190 if ((measurement.getState() 191 & (GnssMeasurement.STATE_GLO_TOD_DECODED 192 | GnssMeasurement.STATE_GLO_TOD_KNOWN)) != 0) { 193 filteredMeasurement.add(measurement); 194 } 195 } else if ((measurement.getState() & (GnssMeasurement.STATE_TOW_DECODED 196 | GnssMeasurement.STATE_TOW_KNOWN)) != 0) { 197 filteredMeasurement.add(measurement); 198 } 199 } 200 return filteredMeasurement; 201 } 202 203 /** 204 * Uses the common reception time approach to calculate pseudorange time 205 * measurements reported by the receiver according to http://cdn.intechopen.com/pdfs-wm/27712.pdf. 206 */ 207 private void validatePseudorange(Collection<GnssMeasurement> measurements, 208 SoftAssert softAssert, long timeInNs) { 209 long largestReceivedSvTimeNanosTod = 0; 210 // closest satellite has largest time (closest to now), as of nano secs of the day 211 // so the largestReceivedSvTimeNanosTod will be the svTime we got from one of the GPS/GLONASS sv 212 for(GnssMeasurement measurement : measurements) { 213 long receivedSvTimeNanosTod = measurement.getReceivedSvTimeNanos() 214 % TimeUnit.DAYS.toNanos(1); 215 if (largestReceivedSvTimeNanosTod < receivedSvTimeNanosTod) { 216 largestReceivedSvTimeNanosTod = receivedSvTimeNanosTod; 217 } 218 } 219 for (GnssMeasurement measurement : measurements) { 220 double threshold = PSEUDORANGE_THRESHOLD_IN_SEC; 221 int constellationType = measurement.getConstellationType(); 222 // BEIDOU and QZSS's Orbit are higher, so the value of ReceivedSvTimeNanos should be small 223 if (constellationType == GnssStatus.CONSTELLATION_BEIDOU 224 || constellationType == GnssStatus.CONSTELLATION_QZSS) { 225 threshold = PSEUDORANGE_THRESHOLD_BEIDOU_QZSS_IN_SEC; 226 } 227 double deltaiNanos = largestReceivedSvTimeNanosTod 228 - (measurement.getReceivedSvTimeNanos() % TimeUnit.DAYS.toNanos(1)); 229 double deltaiSeconds = deltaiNanos * SECONDS_PER_NANO; 230 231 softAssert.assertTrue("deltaiSeconds in Seconds.", 232 timeInNs, 233 "0.0 <= deltaiSeconds <= " + String.valueOf(threshold), 234 String.valueOf(deltaiSeconds), 235 (deltaiSeconds >= 0.0 && deltaiSeconds <= threshold)); 236 } 237 } 238 239 /* 240 * Use pseudorange calculation library to calculate position then compare to location from 241 * Location Manager. 242 */ 243 @CddTest(requirement = "7.3.3") 244 @AppModeFull(reason = "Flaky in instant mode") 245 public void testPseudoPosition() throws Exception { 246 // Checks if Gnss hardware feature is present, skips test (pass) if not, 247 // and hard asserts that Location/Gnss (Provider) is turned on if is Cts Verifier. 248 // From android O, CTS tests should run in the lab with GPS signal. 249 if (!TestMeasurementUtil.canTestRunOnCurrentDevice(mTestLocationManager, true)) { 250 return; 251 } 252 253 mLocationListener = new TestLocationListener(LOCATION_TO_COLLECT_COUNT); 254 mTestLocationManager.requestLocationUpdates(mLocationListener); 255 256 mMeasurementListener = new TestGnssMeasurementListener(TAG, 257 MEASUREMENT_EVENTS_TO_COLLECT_COUNT, true); 258 mTestLocationManager.registerGnssMeasurementCallback(mMeasurementListener); 259 260 boolean success = mLocationListener.await(); 261 262 List<Location> receivedLocationList = mLocationListener.getReceivedLocationList(); 263 assertTrue("Time elapsed without getting enough location fixes." 264 + " Possibly, the test has been run deep indoors." 265 + " Consider retrying test outdoors.", 266 success && receivedLocationList.size() > 0); 267 Location locationFromApi = receivedLocationList.get(0); 268 269 // Since we are checking the eventCount later, there is no need to check the return value 270 // here. 271 mMeasurementListener.await(); 272 273 List<GnssMeasurementsEvent> events = mMeasurementListener.getEvents(); 274 int eventCount = events.size(); 275 Log.i(TAG, "Number of Gps Event received = " + eventCount); 276 277 assertTrue("GnssMeasurementEvent count: expected > 0, received = " + eventCount, 278 eventCount > 0); 279 280 PseudorangePositionVelocityFromRealTimeEvents mPseudorangePositionFromRealTimeEvents 281 = new PseudorangePositionVelocityFromRealTimeEvents(); 282 mPseudorangePositionFromRealTimeEvents.setReferencePosition( 283 (int) (locationFromApi.getLatitude() * 1E7), 284 (int) (locationFromApi.getLongitude() * 1E7), 285 (int) (locationFromApi.getAltitude() * 1E7)); 286 287 Log.i(TAG, "Location from Location Manager" 288 + ", Latitude:" + locationFromApi.getLatitude() 289 + ", Longitude:" + locationFromApi.getLongitude() 290 + ", Altitude:" + locationFromApi.getAltitude()); 291 292 // Ensure at least some calculated locations have a reasonably low uncertainty 293 boolean someLocationsHaveLowPosUnc = false; 294 boolean someLocationsHaveLowVelUnc = false; 295 296 int totalCalculatedLocationCnt = 0; 297 for (GnssMeasurementsEvent event : events) { 298 // In mMeasurementListener.getEvents() we already filtered out events, at this point 299 // every event will have at least 4 satellites in one constellation. 300 mPseudorangePositionFromRealTimeEvents.computePositionVelocitySolutionsFromRawMeas( 301 event); 302 double[] calculatedLatLngAlt = 303 mPseudorangePositionFromRealTimeEvents.getPositionSolutionLatLngDeg(); 304 // it will return NaN when there is no enough measurements to calculate the position 305 if (Double.isNaN(calculatedLatLngAlt[0])) { 306 continue; 307 } else { 308 totalCalculatedLocationCnt++; 309 Log.i(TAG, "Calculated Location" 310 + ", Latitude:" + calculatedLatLngAlt[0] 311 + ", Longitude:" + calculatedLatLngAlt[1] 312 + ", Altitude:" + calculatedLatLngAlt[2]); 313 314 double[] posVelUncertainties = 315 mPseudorangePositionFromRealTimeEvents.getPositionVelocityUncertaintyEnu(); 316 317 double horizontalPositionUncertaintyMeters = 318 Math.sqrt(posVelUncertainties[0] * posVelUncertainties[0] 319 + posVelUncertainties[1] * posVelUncertainties[1]); 320 321 // Tolerate large offsets, when the device reports a large uncertainty - while also 322 // ensuring (here) that some locations are produced before the test ends 323 // with a reasonably low set of error estimates 324 if (horizontalPositionUncertaintyMeters < LOW_ENOUGH_POSITION_UNCERTAINTY_METERS) { 325 someLocationsHaveLowPosUnc = true; 326 } 327 328 // Root-sum-sqaure the WLS, and device generated 68%ile accuracies is a conservative 329 // 68%ile offset (given likely correlated errors) - then this is scaled by 330 // initially 3 sigma to give a high enough tolerance to make the test tolerant 331 // enough of noise to pass reliably. Floor adds additional robustness in case of 332 // small errors and small error estimates. 333 double horizontalOffsetThresholdMeters = HORIZONTAL_OFFSET_SIGMA * Math.sqrt( 334 horizontalPositionUncertaintyMeters * horizontalPositionUncertaintyMeters 335 + locationFromApi.getAccuracy() * locationFromApi.getAccuracy()) 336 + HORIZONTAL_OFFSET_FLOOR_METERS; 337 338 Location calculatedLocation = new Location("gps"); 339 calculatedLocation.setLatitude(calculatedLatLngAlt[0]); 340 calculatedLocation.setLongitude(calculatedLatLngAlt[1]); 341 calculatedLocation.setAltitude(calculatedLatLngAlt[2]); 342 343 double horizontalOffset = calculatedLocation.distanceTo(locationFromApi); 344 345 Log.i(TAG, "Calculated Location Offset: " + horizontalOffset 346 + ", Threshold: " + horizontalOffsetThresholdMeters); 347 assertTrue("Latitude & Longitude calculated from pseudoranges should be close to " 348 + "those reported from Location Manager. Offset = " 349 + horizontalOffset + " meters. Threshold = " 350 + horizontalOffsetThresholdMeters + " meters ", 351 horizontalOffset < horizontalOffsetThresholdMeters); 352 353 //TODO: Check for the altitude offset 354 355 // This 2D velocity uncertainty is conservatively larger than speed uncertainty 356 // as it also contains the effect of bearing uncertainty at a constant speed 357 double horizontalVelocityUncertaintyMps = 358 Math.sqrt(posVelUncertainties[4] * posVelUncertainties[4] 359 + posVelUncertainties[5] * posVelUncertainties[5]); 360 if (horizontalVelocityUncertaintyMps < LOW_ENOUGH_VELOCITY_UNCERTAINTY_MPS) { 361 someLocationsHaveLowVelUnc = true; 362 } 363 364 // Assume 1m/s uncertainty from API, for this test, if not provided 365 float speedUncFromApiMps = locationFromApi.hasSpeedAccuracy() 366 ? locationFromApi.getSpeedAccuracyMetersPerSecond() 367 : HORIZONTAL_OFFSET_FLOOR_MPS; 368 369 // Similar 3-standard deviation plus floor threshold as 370 // horizontalOffsetThresholdMeters above 371 double horizontalSpeedOffsetThresholdMps = HORIZONTAL_OFFSET_SIGMA * Math.sqrt( 372 horizontalVelocityUncertaintyMps * horizontalVelocityUncertaintyMps 373 + speedUncFromApiMps * speedUncFromApiMps) 374 + HORIZONTAL_OFFSET_FLOOR_MPS; 375 376 double[] calculatedVelocityEnuMps = 377 mPseudorangePositionFromRealTimeEvents.getVelocitySolutionEnuMps(); 378 double calculatedHorizontalSpeedMps = 379 Math.sqrt(calculatedVelocityEnuMps[0] * calculatedVelocityEnuMps[0] 380 + calculatedVelocityEnuMps[1] * calculatedVelocityEnuMps[1]); 381 382 Log.i(TAG, "Calculated Speed: " + calculatedHorizontalSpeedMps 383 + ", Reported Speed: " + locationFromApi.getSpeed() 384 + ", Threshold: " + horizontalSpeedOffsetThresholdMps); 385 assertTrue("Speed (" + calculatedHorizontalSpeedMps + " m/s) calculated from" 386 + " pseudoranges should be close to the speed (" 387 + locationFromApi.getSpeed() + " m/s) reported from" 388 + " Location Manager.", 389 Math.abs(calculatedHorizontalSpeedMps - locationFromApi.getSpeed()) 390 < horizontalSpeedOffsetThresholdMps); 391 } 392 } 393 394 assertTrue("Calculated Location Count should be greater than 0.", 395 totalCalculatedLocationCnt > 0); 396 assertTrue("Calculated Horizontal Location Uncertainty should at least once be less than " 397 + LOW_ENOUGH_POSITION_UNCERTAINTY_METERS, 398 someLocationsHaveLowPosUnc); 399 assertTrue("Calculated Horizontal Velocity Uncertainty should at least once be less than " 400 + LOW_ENOUGH_VELOCITY_UNCERTAINTY_MPS, 401 someLocationsHaveLowVelUnc); 402 } 403 } 404