• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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