1 /* 2 * Copyright (C) 2013 The Android Open Source Project 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 package android.hardware.cts.helpers; 17 18 import android.hardware.Sensor; 19 import android.os.Environment; 20 import android.util.Log; 21 import java.io.File; 22 import java.io.IOException; 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.Collections; 26 import java.util.List; 27 import java.util.concurrent.TimeUnit; 28 29 /** 30 * Set of static helper methods for CTS tests. 31 */ 32 //TODO: Refactor this class into several more well defined helper classes, look at StatisticsUtils 33 public class SensorCtsHelper { 34 35 private static final long NANOS_PER_MILLI = 1000000; 36 37 /** 38 * Private constructor for static class. 39 */ SensorCtsHelper()40 private SensorCtsHelper() {} 41 42 /** 43 * Get low and high percentiles values of an array 44 * 45 * @param lowPercentile Lower boundary percentile, range [0, 1] 46 * @param highPercentile Higher boundary percentile, range [0, 1] 47 * 48 * @throws IllegalArgumentException if the collection or percentiles is null or empty. 49 */ getPercentileValue( Collection<TValue> collection, float lowPecentile, float highPercentile)50 public static <TValue extends Comparable<? super TValue>> List<TValue> getPercentileValue( 51 Collection<TValue> collection, float lowPecentile, float highPercentile) { 52 validateCollection(collection); 53 if (lowPecentile > highPercentile || lowPecentile < 0 || highPercentile > 1) { 54 throw new IllegalStateException("percentile has to be in range [0, 1], and " + 55 "lowPecentile has to be less than or equal to highPercentile"); 56 } 57 58 List<TValue> arrayCopy = new ArrayList<TValue>(collection); 59 Collections.sort(arrayCopy); 60 61 List<TValue> percentileValues = new ArrayList<TValue>(); 62 // lower percentile: rounding upwards, index range 1 .. size - 1 for percentile > 0 63 // for percentile == 0, index will be 0. 64 int lowArrayIndex = Math.min(arrayCopy.size() - 1, 65 arrayCopy.size() - (int)(arrayCopy.size() * (1 - lowPecentile))); 66 percentileValues.add(arrayCopy.get(lowArrayIndex)); 67 68 // upper percentile: rounding downwards, index range 0 .. size - 2 for percentile < 1 69 // for percentile == 1, index will be size - 1. 70 // Also, lower bound by lowerArrayIndex to avoid low percentile value being higher than 71 // high percentile value. 72 int highArrayIndex = Math.max(lowArrayIndex, (int)(arrayCopy.size() * highPercentile - 1)); 73 percentileValues.add(arrayCopy.get(highArrayIndex)); 74 return percentileValues; 75 } 76 77 /** 78 * Calculate the mean of a collection. 79 * 80 * @throws IllegalArgumentException if the collection is null or empty 81 */ getMean(Collection<TValue> collection)82 public static <TValue extends Number> double getMean(Collection<TValue> collection) { 83 validateCollection(collection); 84 85 double sum = 0.0; 86 for(TValue value : collection) { 87 sum += value.doubleValue(); 88 } 89 return sum / collection.size(); 90 } 91 92 /** 93 * Calculate the bias-corrected sample variance of a collection. 94 * 95 * @throws IllegalArgumentException if the collection is null or empty 96 */ getVariance(Collection<TValue> collection)97 public static <TValue extends Number> double getVariance(Collection<TValue> collection) { 98 validateCollection(collection); 99 100 double mean = getMean(collection); 101 ArrayList<Double> squaredDiffs = new ArrayList<Double>(); 102 for(TValue value : collection) { 103 double difference = mean - value.doubleValue(); 104 squaredDiffs.add(Math.pow(difference, 2)); 105 } 106 107 double sum = 0.0; 108 for (Double value : squaredDiffs) { 109 sum += value; 110 } 111 return sum / (squaredDiffs.size() - 1); 112 } 113 114 /** 115 * @return The (measured) sampling rate of a collection of {@link TestSensorEvent}. 116 */ getSamplingPeriodNs(List<TestSensorEvent> collection)117 public static long getSamplingPeriodNs(List<TestSensorEvent> collection) { 118 int collectionSize = collection.size(); 119 if (collectionSize < 2) { 120 return 0; 121 } 122 TestSensorEvent firstEvent = collection.get(0); 123 TestSensorEvent lastEvent = collection.get(collectionSize - 1); 124 return (lastEvent.timestamp - firstEvent.timestamp) / (collectionSize - 1); 125 } 126 127 /** 128 * Calculate the bias-corrected standard deviation of a collection. 129 * 130 * @throws IllegalArgumentException if the collection is null or empty 131 */ getStandardDeviation( Collection<TValue> collection)132 public static <TValue extends Number> double getStandardDeviation( 133 Collection<TValue> collection) { 134 return Math.sqrt(getVariance(collection)); 135 } 136 137 /** 138 * Convert a period to frequency in Hz. 139 */ getFrequency(TValue period, TimeUnit unit)140 public static <TValue extends Number> double getFrequency(TValue period, TimeUnit unit) { 141 return 1000000000 / (TimeUnit.NANOSECONDS.convert(1, unit) * period.doubleValue()); 142 } 143 144 /** 145 * Convert a frequency in Hz into a period. 146 */ getPeriod(TValue frequency, TimeUnit unit)147 public static <TValue extends Number> double getPeriod(TValue frequency, TimeUnit unit) { 148 return 1000000000 / (TimeUnit.NANOSECONDS.convert(1, unit) * frequency.doubleValue()); 149 } 150 151 /** 152 * If value lies outside the boundary limit, then return the nearer bound value. 153 * Otherwise, return the value unchanged. 154 */ clamp(TValue val, TValue min, TValue max)155 public static <TValue extends Number> double clamp(TValue val, TValue min, TValue max) { 156 return Math.min(max.doubleValue(), Math.max(min.doubleValue(), val.doubleValue())); 157 } 158 159 /** 160 * @return The magnitude (norm) represented by the given array of values. 161 */ getMagnitude(float[] values)162 public static double getMagnitude(float[] values) { 163 float sumOfSquares = 0.0f; 164 for (float value : values) { 165 sumOfSquares += value * value; 166 } 167 double magnitude = Math.sqrt(sumOfSquares); 168 return magnitude; 169 } 170 171 /** 172 * Helper method to sleep for a given duration. 173 */ sleep(long duration, TimeUnit timeUnit)174 public static void sleep(long duration, TimeUnit timeUnit) throws InterruptedException { 175 long durationNs = TimeUnit.NANOSECONDS.convert(duration, timeUnit); 176 Thread.sleep(durationNs / NANOS_PER_MILLI, (int) (durationNs % NANOS_PER_MILLI)); 177 } 178 179 /** 180 * Format an assertion message. 181 * 182 * @param label the verification name 183 * @param environment the environment of the test 184 * 185 * @return The formatted string 186 */ formatAssertionMessage(String label, TestSensorEnvironment environment)187 public static String formatAssertionMessage(String label, TestSensorEnvironment environment) { 188 return formatAssertionMessage(label, environment, "Failed"); 189 } 190 191 /** 192 * Format an assertion message with a custom message. 193 * 194 * @param label the verification name 195 * @param environment the environment of the test 196 * @param format the additional format string 197 * @param params the additional format params 198 * 199 * @return The formatted string 200 */ formatAssertionMessage( String label, TestSensorEnvironment environment, String format, Object ... params)201 public static String formatAssertionMessage( 202 String label, 203 TestSensorEnvironment environment, 204 String format, 205 Object ... params) { 206 return formatAssertionMessage(label, environment, String.format(format, params)); 207 } 208 209 /** 210 * Format an assertion message. 211 * 212 * @param label the verification name 213 * @param environment the environment of the test 214 * @param extras the additional information for the assertion 215 * 216 * @return The formatted string 217 */ formatAssertionMessage( String label, TestSensorEnvironment environment, String extras)218 public static String formatAssertionMessage( 219 String label, 220 TestSensorEnvironment environment, 221 String extras) { 222 return String.format( 223 "%s | sensor='%s', samplingPeriod=%dus, maxReportLatency=%dus | %s", 224 label, 225 environment.getSensor().getName(), 226 environment.getRequestedSamplingPeriodUs(), 227 environment.getMaxReportLatencyUs(), 228 extras); 229 } 230 231 /** 232 * Format an array of floats. 233 * 234 * @param array the array of floats 235 * 236 * @return The formatted string 237 */ formatFloatArray(float[] array)238 public static String formatFloatArray(float[] array) { 239 StringBuilder sb = new StringBuilder(); 240 if (array.length > 1) { 241 sb.append("("); 242 } 243 for (int i = 0; i < array.length; i++) { 244 sb.append(String.format("%.2f", array[i])); 245 if (i != array.length - 1) { 246 sb.append(", "); 247 } 248 } 249 if (array.length > 1) { 250 sb.append(")"); 251 } 252 return sb.toString(); 253 } 254 255 /** 256 * @return A {@link File} representing a root directory to store sensor tests data. 257 */ getSensorTestDataDirectory()258 public static File getSensorTestDataDirectory() throws IOException { 259 File dataDirectory = new File(Environment.getExternalStorageDirectory(), "sensorTests/"); 260 return createDirectoryStructure(dataDirectory); 261 } 262 263 /** 264 * Creates the directory structure for the given sensor test data sub-directory. 265 * 266 * @param subdirectory The sub-directory's name. 267 */ getSensorTestDataDirectory(String subdirectory)268 public static File getSensorTestDataDirectory(String subdirectory) throws IOException { 269 File subdirectoryFile = new File(getSensorTestDataDirectory(), subdirectory); 270 return createDirectoryStructure(subdirectoryFile); 271 } 272 273 /** 274 * Sanitizes a string so it can be used in file names. 275 * 276 * @param value The string to sanitize. 277 * @return The sanitized string. 278 * 279 * @throws SensorTestPlatformException If the string cannot be sanitized. 280 */ sanitizeStringForFileName(String value)281 public static String sanitizeStringForFileName(String value) 282 throws SensorTestPlatformException { 283 String sanitizedValue = value.replaceAll("[^a-zA-Z0-9_\\-]", "_"); 284 if (sanitizedValue.matches("_*")) { 285 throw new SensorTestPlatformException( 286 "Unable to sanitize string '%s' for file name.", 287 value); 288 } 289 return sanitizedValue; 290 } 291 292 /** 293 * Ensures that the directory structure represented by the given {@link File} is created. 294 */ createDirectoryStructure(File directoryStructure)295 private static File createDirectoryStructure(File directoryStructure) throws IOException { 296 directoryStructure.mkdirs(); 297 if (!directoryStructure.isDirectory()) { 298 throw new IOException("Unable to create directory structure for " 299 + directoryStructure.getAbsolutePath()); 300 } 301 return directoryStructure; 302 } 303 304 /** 305 * Validate that a collection is not null or empty. 306 * 307 * @throws IllegalStateException if collection is null or empty. 308 */ validateCollection(Collection<T> collection)309 private static <T> void validateCollection(Collection<T> collection) { 310 if(collection == null || collection.size() == 0) { 311 throw new IllegalStateException("Collection cannot be null or empty"); 312 } 313 } 314 getUnitsForSensor(Sensor sensor)315 public static String getUnitsForSensor(Sensor sensor) { 316 switch(sensor.getType()) { 317 case Sensor.TYPE_ACCELEROMETER: 318 return "m/s^2"; 319 case Sensor.TYPE_MAGNETIC_FIELD: 320 case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED: 321 return "uT"; 322 case Sensor.TYPE_GYROSCOPE: 323 case Sensor.TYPE_GYROSCOPE_UNCALIBRATED: 324 return "radians/sec"; 325 case Sensor.TYPE_PRESSURE: 326 return "hPa"; 327 }; 328 return ""; 329 } 330 hasResolutionRequirement(Sensor sensor, boolean hasHifiSensors)331 public static boolean hasResolutionRequirement(Sensor sensor, boolean hasHifiSensors) { 332 switch (sensor.getType()) { 333 case Sensor.TYPE_ACCELEROMETER: 334 case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED: 335 case Sensor.TYPE_GYROSCOPE: 336 case Sensor.TYPE_GYROSCOPE_UNCALIBRATED: 337 case Sensor.TYPE_MAGNETIC_FIELD: 338 case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED: 339 return true; 340 341 case Sensor.TYPE_PRESSURE: 342 // Pressure sensor only has a resolution requirement when there are HiFi sensors 343 return hasHifiSensors; 344 } 345 return false; 346 } 347 getRequiredResolutionForSensor(Sensor sensor)348 public static float getRequiredResolutionForSensor(Sensor sensor) { 349 switch (sensor.getType()) { 350 case Sensor.TYPE_ACCELEROMETER: 351 case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED: 352 case Sensor.TYPE_GYROSCOPE: 353 case Sensor.TYPE_GYROSCOPE_UNCALIBRATED: 354 // Accelerometer and gyroscope must have at least 12 bits 355 // of resolution. The maximum resolution calculation uses 356 // slightly more than twice the maximum range because 357 // 1) the sensor must be able to report values from 358 // [-maxRange, maxRange] without saturating 359 // 2) to allow for slight rounding errors 360 return (float)(2.001f * sensor.getMaximumRange() / Math.pow(2, 12)); 361 case Sensor.TYPE_MAGNETIC_FIELD: 362 case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED: 363 // Magnetometer must have a resolution equal to or denser 364 // than 0.6 uT 365 return 0.6f; 366 case Sensor.TYPE_PRESSURE: 367 // Pressure sensor must have at least 80 LSB / hPa which is 368 // equivalent to 0.0125 hPa / LSB. Allow for a small margin of 369 // error due to rounding errors. 370 return 1.01f * (1.0f / 80.0f); 371 } 372 return 0.0f; 373 } 374 sensorTypeShortString(int type)375 public static String sensorTypeShortString(int type) { 376 switch (type) { 377 case Sensor.TYPE_ACCELEROMETER: 378 return "Accel"; 379 case Sensor.TYPE_GYROSCOPE: 380 return "Gyro"; 381 case Sensor.TYPE_MAGNETIC_FIELD: 382 return "Mag"; 383 case Sensor.TYPE_ACCELEROMETER_UNCALIBRATED: 384 return "UncalAccel"; 385 case Sensor.TYPE_GYROSCOPE_UNCALIBRATED: 386 return "UncalGyro"; 387 case Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED: 388 return "UncalMag"; 389 default: 390 return "Type_" + type; 391 } 392 } 393 394 public static class TestResultCollector { 395 private List<AssertionError> mErrorList = new ArrayList<>(); 396 private List<String> mErrorStringList = new ArrayList<>(); 397 private String mTestName; 398 private String mTag; 399 TestResultCollector()400 public TestResultCollector() { 401 this("Test"); 402 } 403 TestResultCollector(String test)404 public TestResultCollector(String test) { 405 this(test, "SensorCtsTest"); 406 } 407 TestResultCollector(String test, String tag)408 public TestResultCollector(String test, String tag) { 409 mTestName = test; 410 mTag = tag; 411 } 412 perform(Runnable r)413 public void perform(Runnable r) { 414 perform(r, ""); 415 } 416 perform(Runnable r, String s)417 public void perform(Runnable r, String s) { 418 try { 419 Log.d(mTag, mTestName + " running " + (s.isEmpty() ? "..." : s)); 420 r.run(); 421 } catch (AssertionError e) { 422 mErrorList.add(e); 423 mErrorStringList.add(s); 424 Log.e(mTag, mTestName + " error: " + e.getMessage()); 425 } 426 } 427 judge()428 public void judge() throws AssertionError { 429 if (mErrorList.isEmpty() && mErrorStringList.isEmpty()) { 430 return; 431 } 432 433 if (mErrorList.size() != mErrorStringList.size()) { 434 throw new IllegalStateException("Mismatch error and error message"); 435 } 436 437 StringBuffer buf = new StringBuffer(); 438 for (int i = 0; i < mErrorList.size(); ++i) { 439 buf.append("Test (").append(mErrorStringList.get(i)).append(") - Error: ") 440 .append(mErrorList.get(i).getMessage()).append("; "); 441 } 442 throw new AssertionError(buf.toString()); 443 } 444 } 445 bytesToHex(byte[] bytes, int offset, int length)446 public static String bytesToHex(byte[] bytes, int offset, int length) { 447 if (offset == -1) { 448 offset = 0; 449 } 450 451 if (length == -1) { 452 length = bytes.length; 453 } 454 455 final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; 456 char[] hexChars = new char[length * 3]; 457 int v; 458 for (int i = 0; i < length; i++) { 459 v = bytes[offset + i] & 0xFF; 460 hexChars[i * 3] = hexArray[v >>> 4]; 461 hexChars[i * 3 + 1] = hexArray[v & 0x0F]; 462 hexChars[i * 3 + 2] = ' '; 463 } 464 return new String(hexChars); 465 } 466 } 467