Home | History | Annotate | Download | only in cts
      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