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