1 /* 2 * Copyright (C) 2015 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 17 package org.chromium.latency.walt; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.graphics.Color; 23 import android.os.Bundle; 24 import android.support.v4.app.Fragment; 25 import android.text.method.ScrollingMovementMethod; 26 import android.view.LayoutInflater; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.widget.TextView; 31 32 import com.github.mikephil.charting.charts.ScatterChart; 33 import com.github.mikephil.charting.components.Description; 34 import com.github.mikephil.charting.data.Entry; 35 import com.github.mikephil.charting.data.ScatterData; 36 import com.github.mikephil.charting.data.ScatterDataSet; 37 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.Locale; 41 42 public class DragLatencyFragment extends Fragment 43 implements View.OnClickListener, RobotAutomationListener { 44 45 private SimpleLogger logger; 46 private WaltDevice waltDevice; 47 private TextView logTextView; 48 private TouchCatcherView touchCatcher; 49 private TextView crossCountsView; 50 private TextView dragCountsView; 51 private View startButton; 52 private View restartButton; 53 private View finishButton; 54 private ScatterChart latencyChart; 55 private View latencyChartLayout; 56 int moveCount = 0; 57 58 ArrayList<UsMotionEvent> touchEventList = new ArrayList<>(); 59 ArrayList<WaltDevice.TriggerMessage> laserEventList = new ArrayList<>(); 60 61 62 private BroadcastReceiver logReceiver = new BroadcastReceiver() { 63 @Override 64 public void onReceive(Context context, Intent intent) { 65 String msg = intent.getStringExtra("message"); 66 DragLatencyFragment.this.appendLogText(msg); 67 } 68 }; 69 70 private View.OnTouchListener touchListener = new View.OnTouchListener() { 71 @Override 72 public boolean onTouch(View v, MotionEvent event) { 73 int histLen = event.getHistorySize(); 74 for (int i = 0; i < histLen; i++){ 75 UsMotionEvent eh = new UsMotionEvent(event, waltDevice.clock.baseTime, i); 76 touchEventList.add(eh); 77 } 78 UsMotionEvent e = new UsMotionEvent(event, waltDevice.clock.baseTime); 79 touchEventList.add(e); 80 moveCount += histLen + 1; 81 82 updateCountsDisplay(); 83 return true; 84 } 85 }; 86 DragLatencyFragment()87 public DragLatencyFragment() { 88 // Required empty public constructor 89 } 90 91 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)92 public View onCreateView(LayoutInflater inflater, ViewGroup container, 93 Bundle savedInstanceState) { 94 logger = SimpleLogger.getInstance(getContext()); 95 waltDevice = WaltDevice.getInstance(getContext()); 96 97 // Inflate the layout for this fragment 98 final View view = inflater.inflate(R.layout.fragment_drag_latency, container, false); 99 logTextView = (TextView) view.findViewById(R.id.txt_log_drag_latency); 100 startButton = view.findViewById(R.id.button_start_drag); 101 restartButton = view.findViewById(R.id.button_restart_drag); 102 finishButton = view.findViewById(R.id.button_finish_drag); 103 touchCatcher = (TouchCatcherView) view.findViewById(R.id.tap_catcher); 104 crossCountsView = (TextView) view.findViewById(R.id.txt_cross_counts); 105 dragCountsView = (TextView) view.findViewById(R.id.txt_drag_counts); 106 latencyChart = (ScatterChart) view.findViewById(R.id.latency_chart); 107 latencyChartLayout = view.findViewById(R.id.latency_chart_layout); 108 logTextView.setMovementMethod(new ScrollingMovementMethod()); 109 view.findViewById(R.id.button_close_chart).setOnClickListener(this); 110 restartButton.setEnabled(false); 111 finishButton.setEnabled(false); 112 return view; 113 } 114 115 @Override onResume()116 public void onResume() { 117 super.onResume(); 118 119 logTextView.setText(logger.getLogText()); 120 logger.registerReceiver(logReceiver); 121 122 // Register this fragment class as the listener for some button clicks 123 startButton.setOnClickListener(this); 124 restartButton.setOnClickListener(this); 125 finishButton.setOnClickListener(this); 126 } 127 128 @Override onPause()129 public void onPause() { 130 logger.unregisterReceiver(logReceiver); 131 super.onPause(); 132 } 133 appendLogText(String msg)134 public void appendLogText(String msg) { 135 logTextView.append(msg + "\n"); 136 } 137 updateCountsDisplay()138 void updateCountsDisplay() { 139 crossCountsView.setText(String.format(Locale.US, "↕ %d", laserEventList.size())); 140 dragCountsView.setText(String.format(Locale.US, "⇄ %d", moveCount)); 141 } 142 143 /** 144 * @return true if measurement was successfully started 145 */ startMeasurement()146 boolean startMeasurement() { 147 logger.log("Starting drag latency test"); 148 try { 149 waltDevice.syncClock(); 150 } catch (IOException e) { 151 logger.log("Error syncing clocks: " + e.getMessage()); 152 return false; 153 } 154 // Register a callback for triggers 155 waltDevice.setTriggerHandler(triggerHandler); 156 try { 157 waltDevice.command(WaltDevice.CMD_AUTO_LASER_ON); 158 waltDevice.startListener(); 159 } catch (IOException e) { 160 logger.log("Error: " + e.getMessage()); 161 waltDevice.clearTriggerHandler(); 162 return false; 163 } 164 touchCatcher.setOnTouchListener(touchListener); 165 touchCatcher.startAnimation(); 166 touchEventList.clear(); 167 laserEventList.clear(); 168 moveCount = 0; 169 updateCountsDisplay(); 170 return true; 171 } 172 restartMeasurement()173 void restartMeasurement() { 174 logger.log("\n## Restarting drag latency test. Re-sync clocks ..."); 175 try { 176 waltDevice.syncClock(); 177 } catch (IOException e) { 178 logger.log("Error syncing clocks: " + e.getMessage()); 179 } 180 181 touchCatcher.startAnimation(); 182 touchEventList.clear(); 183 laserEventList.clear(); 184 moveCount = 0; 185 updateCountsDisplay(); 186 } 187 finishAndShowStats()188 void finishAndShowStats() { 189 touchCatcher.stopAnimation(); 190 waltDevice.stopListener(); 191 try { 192 waltDevice.command(WaltDevice.CMD_AUTO_LASER_OFF); 193 } catch (IOException e) { 194 logger.log("Error: " + e.getMessage()); 195 } 196 touchCatcher.setOnTouchListener(null); 197 waltDevice.clearTriggerHandler(); 198 199 waltDevice.checkDrift(); 200 201 logger.log(String.format(Locale.US, 202 "Recorded %d laser events and %d touch events. ", 203 laserEventList.size(), 204 touchEventList.size() 205 )); 206 207 if (touchEventList.size() < 100) { 208 logger.log("Insufficient number of touch events (<100), aborting."); 209 return; 210 } 211 212 if (laserEventList.size() < 8) { 213 logger.log("Insufficient number of laser events (<8), aborting."); 214 return; 215 } 216 217 // TODO: Log raw data if enabled in settings, touch events add lots of text to the log. 218 // logRawData(); 219 reshapeAndCalculate(); 220 LogUploader.uploadIfAutoEnabled(getContext()); 221 } 222 223 // Data formatted for processing with python script, y.py logRawData()224 void logRawData() { 225 logger.log("#####> LASER EVENTS #####"); 226 for (int i = 0; i < laserEventList.size(); i++){ 227 logger.log(laserEventList.get(i).t + " " + laserEventList.get(i).value); 228 } 229 logger.log("#####< END OF LASER EVENTS #####"); 230 231 logger.log("=====> TOUCH EVENTS ====="); 232 for (UsMotionEvent e: touchEventList) { 233 logger.log(String.format(Locale.US, 234 "%d %.3f %.3f", 235 e.kernelTime, 236 e.x, e.y 237 )); 238 } 239 logger.log("=====< END OF TOUCH EVENTS ====="); 240 } 241 reshapeAndCalculate()242 void reshapeAndCalculate() { 243 double[] ft, lt; // All time arrays are in _milliseconds_ 244 double[] fy; 245 int[] ldir; 246 247 // Use the time of the first touch event as time = 0 for debugging convenience 248 long t0_us = touchEventList.get(0).kernelTime; 249 long tLast_us = touchEventList.get(touchEventList.size() - 1).kernelTime; 250 251 int fN = touchEventList.size(); 252 ft = new double[fN]; 253 fy = new double[fN]; 254 255 for (int i = 0; i < fN; i++){ 256 ft[i] = (touchEventList.get(i).kernelTime - t0_us) / 1000.; 257 fy[i] = touchEventList.get(i).y; 258 } 259 260 // Remove all laser events that are outside the time span of the touch events 261 // they are not usable and would result in errors downstream 262 int j = laserEventList.size() - 1; 263 while (j >= 0 && laserEventList.get(j).t > tLast_us) { 264 laserEventList.remove(j); 265 j--; 266 } 267 268 while (laserEventList.size() > 0 && laserEventList.get(0).t < t0_us) { 269 laserEventList.remove(0); 270 } 271 272 // Calculation assumes that the first event is generated by the finger obstructing the beam. 273 // Remove the first event if it was generated by finger going out of the beam (value==1). 274 while (laserEventList.size() > 0 && laserEventList.get(0).value == 1) { 275 laserEventList.remove(0); 276 } 277 278 int lN = laserEventList.size(); 279 280 if (lN < 8) { 281 logger.log("ERROR: Insufficient number of laser events overlapping with touch events," + 282 "aborting." 283 ); 284 return; 285 } 286 287 lt = new double[lN]; 288 ldir = new int[lN]; 289 for (int i = 0; i < lN; i++){ 290 lt[i] = (laserEventList.get(i).t - t0_us) / 1000.; 291 ldir[i] = laserEventList.get(i).value; 292 } 293 294 calculateDragLatency(ft,fy, lt, ldir); 295 } 296 297 /** 298 * Handler for all the button clicks on this screen. 299 */ 300 @Override onClick(View v)301 public void onClick(View v) { 302 if (v.getId() == R.id.button_restart_drag) { 303 latencyChartLayout.setVisibility(View.GONE); 304 restartButton.setEnabled(false); 305 restartMeasurement(); 306 restartButton.setEnabled(true); 307 return; 308 } 309 310 if (v.getId() == R.id.button_start_drag) { 311 latencyChartLayout.setVisibility(View.GONE); 312 startButton.setEnabled(false); 313 boolean startSuccess = startMeasurement(); 314 if (startSuccess) { 315 finishButton.setEnabled(true); 316 restartButton.setEnabled(true); 317 } else { 318 startButton.setEnabled(true); 319 } 320 return; 321 } 322 323 if (v.getId() == R.id.button_finish_drag) { 324 finishButton.setEnabled(false); 325 restartButton.setEnabled(false); 326 finishAndShowStats(); 327 startButton.setEnabled(true); 328 return; 329 } 330 331 if (v.getId() == R.id.button_close_chart) { 332 latencyChartLayout.setVisibility(View.GONE); 333 } 334 } 335 onRobotAutomationEvent(String event)336 public void onRobotAutomationEvent(String event) { 337 if (event.equals(RobotAutomationListener.RESTART_EVENT)) { 338 onClick(restartButton); 339 } else if (event.equals(RobotAutomationListener.START_EVENT)) { 340 onClick(startButton); 341 } else if (event.equals(RobotAutomationListener.FINISH_EVENT)) { 342 onClick(finishButton); 343 } 344 } 345 346 private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() { 347 @Override 348 public void onReceive(WaltDevice.TriggerMessage tmsg) { 349 laserEventList.add(tmsg); 350 updateCountsDisplay(); 351 } 352 }; 353 calculateDragLatency(double[] ft, double[] fy, double[] lt, int[] ldir)354 public void calculateDragLatency(double[] ft, double[] fy, double[] lt, int[] ldir) { 355 // TODO: throw away several first laser crossings (if not already) 356 double[] ly = Utils.interp(lt, ft, fy); 357 double lmid = Utils.mean(ly); 358 // Assume first crossing is into the beam = light-off = 0 359 if (ldir[0] != 0) { 360 // TODO: add more sanity checks here. 361 logger.log("First laser crossing is not into the beam, aborting"); 362 return; 363 } 364 365 // label sides, one simple label is i starts from 1, then side = (i mod 4) / 2 same as the 2nd LSB bit or i. 366 int[] sideIdx = new int[lt.length]; 367 368 // This is one way of deciding what laser events were on which side 369 // It should go above, below, below, above, above 370 // The other option is to mirror the python code that uses position and velocity for this 371 for (int i = 0; i<lt.length; i++) { 372 sideIdx[i] = ((i+1) / 2) % 2; 373 } 374 /* 375 logger.log("ft = " + Utils.array2string(ft, "%.2f")); 376 logger.log("fy = " + Utils.array2string(fy, "%.2f")); 377 logger.log("lt = " + Utils.array2string(lt, "%.2f")); 378 logger.log("sideIdx = " + Arrays.toString(sideIdx));*/ 379 380 double averageBestShift = 0; 381 for(int side = 0; side < 2; side++) { 382 double[] lts = Utils.extract(sideIdx, side, lt); 383 // TODO: time this call 384 double bestShift = Utils.findBestShift(lts, ft, fy); 385 logger.log(String.format(Locale.US, "bestShift = %.2f", bestShift)); 386 averageBestShift += bestShift / 2; 387 } 388 389 drawLatencyGraph(ft, fy, lt, averageBestShift); 390 logger.log(String.format(Locale.US, "Drag latency is %.1f [ms]", averageBestShift)); 391 } 392 drawLatencyGraph(double[] ft, double[] fy, double[] lt, double averageBestShift)393 private void drawLatencyGraph(double[] ft, double[] fy, double[] lt, double averageBestShift) { 394 final ArrayList<Entry> touchEntries = new ArrayList<>(); 395 final ArrayList<Entry> laserEntries = new ArrayList<>(); 396 final double[] laserT = new double[lt.length]; 397 for (int i = 0; i < ft.length; i++) { 398 touchEntries.add(new Entry((float) ft[i], (float) fy[i])); 399 } 400 for (int i = 0; i < lt.length; i++) { 401 laserT[i] = lt[i] + averageBestShift; 402 } 403 final double[] laserY = Utils.interp(laserT, ft, fy); 404 for (int i = 0; i < laserY.length; i++) { 405 laserEntries.add(new Entry((float) laserT[i], (float) laserY[i])); 406 } 407 408 final ScatterDataSet dataSetTouch = new ScatterDataSet(touchEntries, "Touch Events"); 409 dataSetTouch.setScatterShape(ScatterChart.ScatterShape.CIRCLE); 410 dataSetTouch.setScatterShapeSize(8f); 411 412 final ScatterDataSet dataSetLaser = new ScatterDataSet(laserEntries, 413 String.format(Locale.US, "Laser Events Latency=%.1f ms", averageBestShift)); 414 dataSetLaser.setColor(Color.RED); 415 dataSetLaser.setScatterShapeSize(10f); 416 dataSetLaser.setScatterShape(ScatterChart.ScatterShape.X); 417 418 final ScatterData scatterData = new ScatterData(dataSetTouch, dataSetLaser); 419 final Description desc = new Description(); 420 desc.setText("Y-Position [pixels] vs. Time [ms]"); 421 desc.setTextSize(12f); 422 latencyChart.setDescription(desc); 423 latencyChart.setData(scatterData); 424 latencyChartLayout.setVisibility(View.VISIBLE); 425 } 426 } 427