• 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.Context;
20 import android.content.res.Resources;
21 import android.hardware.usb.UsbDevice;
22 import android.os.Handler;
23 import android.util.Log;
24 
25 import java.io.IOException;
26 
27 /**
28  * A singleton used as an interface for the physical WALT device.
29  */
30 public class WaltDevice implements WaltConnection.ConnectionStateListener {
31 
32     private static final int DEFAULT_DRIFT_LIMIT_US = 1500;
33     private static final String TAG = "WaltDevice";
34     public static final String PROTOCOL_VERSION = "5";
35 
36     // Teensy side commands. Each command is a single char
37     // Based on #defines section in walt.ino
38     static final char CMD_PING_DELAYED     = 'D'; // Ping with a delay
39     static final char CMD_RESET            = 'F'; // Reset all vars
40     static final char CMD_SYNC_SEND        = 'I'; // Send some digits for clock sync
41     static final char CMD_PING             = 'P'; // Ping with a single byte
42     static final char CMD_VERSION          = 'V'; // Determine WALT's firmware version
43     static final char CMD_SYNC_READOUT     = 'R'; // Read out sync times
44     static final char CMD_GSHOCK           = 'G'; // Send last shock time and watch for another shock.
45     static final char CMD_TIME_NOW         = 'T'; // Current time
46     static final char CMD_SYNC_ZERO        = 'Z'; // Initial zero
47     static final char CMD_AUTO_SCREEN_ON   = 'C'; // Send a message on screen color change
48     static final char CMD_AUTO_SCREEN_OFF  = 'c';
49     static final char CMD_SEND_LAST_SCREEN = 'E'; // Send info about last screen color change
50     static final char CMD_BRIGHTNESS_CURVE = 'U'; // Probe screen for brightness vs time curve
51     static final char CMD_AUTO_LASER_ON    = 'L'; // Send messages on state change of the laser
52     static final char CMD_AUTO_LASER_OFF   = 'l';
53     static final char CMD_SEND_LAST_LASER  = 'J';
54     static final char CMD_AUDIO            = 'A'; // Start watching for signal on audio out line
55     static final char CMD_BEEP             = 'B'; // Generate a tone into the mic and send timestamp
56     static final char CMD_BEEP_STOP        = 'S'; // Stop generating tone
57     static final char CMD_MIDI             = 'M'; // Start listening for a MIDI message
58     static final char CMD_NOTE             = 'N'; // Generate a MIDI NoteOn message
59 
60     private static final int BYTE_BUFFER_SIZE = 1024 * 4;
61     private byte[] buffer = new byte[BYTE_BUFFER_SIZE];
62 
63     private Context context;
64     protected SimpleLogger logger;
65     private WaltConnection connection;
66     public RemoteClockInfo clock;
67     private WaltConnection.ConnectionStateListener connectionStateListener;
68 
69     private static final Object LOCK = new Object();
70     private static WaltDevice instance;
71 
getInstance(Context context)72     public static WaltDevice getInstance(Context context) {
73         synchronized (LOCK) {
74             if (instance == null) {
75                 instance = new WaltDevice(context.getApplicationContext());
76             }
77             return instance;
78         }
79     }
80 
WaltDevice(Context context)81     private WaltDevice(Context context) {
82         this.context = context;
83         triggerListener = new TriggerListener();
84         logger = SimpleLogger.getInstance(context);
85     }
86 
onConnect()87     public void onConnect() {
88         try {
89             // TODO: restore
90             softReset();
91             checkVersion();
92             syncClock();
93         } catch (IOException e) {
94             logger.log("Unable to communicate with WALT: " + e.getMessage());
95         }
96 
97         if (connectionStateListener != null) {
98             connectionStateListener.onConnect();
99         }
100     }
101 
102     // Called when disconnecting from WALT
103     // TODO: restore this, not called from anywhere
onDisconnect()104     public void onDisconnect() {
105         if (!isListenerStopped()) {
106             stopListener();
107         }
108 
109         if (connectionStateListener != null) {
110             connectionStateListener.onDisconnect();
111         }
112     }
113 
connect()114     public void connect() {
115         if (WaltTcpConnection.probe()) {
116             logger.log("Using TCP bridge for ChromeOS");
117             connection = WaltTcpConnection.getInstance(context);
118         } else {
119             // USB connection
120             logger.log("No TCP bridge detected, using direct USB connection");
121             connection = WaltUsbConnection.getInstance(context);
122         }
123         connection.setConnectionStateListener(this);
124         connection.connect();
125     }
126 
connect(UsbDevice usbDevice)127     public void connect(UsbDevice usbDevice) {
128         // This happens when apps starts as a result of plugging WALT into USB. In this case we
129         // receive an intent with a usbDevice
130         WaltUsbConnection usbConnection = WaltUsbConnection.getInstance(context);
131         connection = usbConnection;
132         connection.setConnectionStateListener(this);
133         usbConnection.connect(usbDevice);
134     }
135 
isConnected()136     public boolean isConnected() {
137         return connection.isConnected();
138     }
139 
140 
readOne()141     public String readOne() throws IOException {
142         if (!isListenerStopped()) {
143             throw new IOException("Can't do blocking read while listener is running");
144         }
145 
146         byte[] buff = new byte[64];
147         int ret = connection.blockingRead(buff);
148 
149         if (ret < 0) {
150             throw new IOException("Timed out reading from WALT");
151         }
152         String s = new String(buff, 0, ret);
153         Log.i(TAG, "readOne() received data: " + s);
154         return s;
155     }
156 
157 
sendReceive(char c)158     private String sendReceive(char c) throws IOException {
159         synchronized (connection) {
160             connection.sendByte(c);
161             return readOne();
162         }
163     }
164 
sendAndFlush(char c)165     public void sendAndFlush(char c) {
166 
167         try {
168             synchronized (connection) {
169                 connection.sendByte(c);
170                 while (connection.blockingRead(buffer) > 0) {
171                     // flushing all incoming data
172                 }
173             }
174         } catch (Exception e) {
175             logger.log("Exception in sendAndFlush: " + e.getMessage());
176             e.printStackTrace();
177         }
178     }
179 
softReset()180     public void softReset() {
181         sendAndFlush(CMD_RESET);
182     }
183 
command(char cmd, char ack)184     String command(char cmd, char ack) throws IOException {
185         if (!isListenerStopped()) {
186             connection.sendByte(cmd); // TODO: check response even if the listener is running
187             return "";
188         }
189         String response = sendReceive(cmd);
190         if (!response.startsWith(String.valueOf(ack))) {
191             throw new IOException("Unexpected response from WALT. Expected \"" + ack
192                     + "\", got \"" + response + "\"");
193         }
194         // Trim out the ack
195         return response.substring(1).trim();
196     }
197 
command(char cmd)198     String command(char cmd) throws IOException {
199         return command(cmd, flipCase(cmd));
200     }
201 
flipCase(char c)202     private char flipCase(char c) {
203         if (Character.isUpperCase(c)) {
204             return Character.toLowerCase(c);
205         } else if (Character.isLowerCase(c)) {
206             return Character.toUpperCase(c);
207         } else {
208             return c;
209         }
210     }
211 
checkVersion()212     public void checkVersion() throws IOException {
213         if (!isConnected()) throw new IOException("Not connected to WALT");
214         if (!isListenerStopped()) throw new IOException("Listener is running");
215 
216         String s = command(CMD_VERSION);
217         if (!PROTOCOL_VERSION.equals(s)) {
218             Resources res = context.getResources();
219             throw new IOException(String.format(res.getString(R.string.protocol_version_mismatch),
220                     s, PROTOCOL_VERSION));
221         }
222     }
223 
syncClock()224     public void syncClock() throws IOException {
225         clock = connection.syncClock();
226     }
227 
228     // Simple way of syncing clocks. Used for diagnostics. Accuracy of several ms.
simpleSyncClock()229     public void simpleSyncClock() throws IOException {
230         byte[] buffer = new byte[1024];
231         clock = new RemoteClockInfo();
232         clock.baseTime = RemoteClockInfo.microTime();
233         String reply = sendReceive(CMD_SYNC_ZERO);
234         logger.log("Simple sync reply: " + reply);
235         clock.maxLag = (int) clock.micros();
236         logger.log("Synced clocks, the simple way:\n" + clock);
237     }
238 
checkDrift()239     public void checkDrift() {
240         if (! isConnected()) {
241             logger.log("ERROR: Not connected, aborting checkDrift()");
242             return;
243         }
244         connection.updateLag();
245         if (clock == null) {
246             // updateLag() will have logged a message if we get here
247             return;
248         }
249         int drift = Math.abs(clock.getMeanLag());
250         String msg = String.format("Remote clock delayed between %d and %d us",
251                 clock.minLag, clock.maxLag);
252         // TODO: Convert the limit to user editable preference
253         if (drift > DEFAULT_DRIFT_LIMIT_US) {
254             msg = "WARNING: High clock drift. " + msg;
255         }
256         logger.log(msg);
257     }
258 
readLastShockTime_mock()259     public long readLastShockTime_mock() {
260         return clock.micros() - 15000;
261     }
262 
readLastShockTime()263     public long readLastShockTime() {
264         String s;
265         try {
266             s = sendReceive(CMD_GSHOCK);
267         } catch (IOException e) {
268             logger.log("Error sending GSHOCK command: " + e.getMessage());
269             return -1;
270         }
271         Log.i(TAG, "Received S reply: " + s);
272         long t = 0;
273         try {
274             t = Integer.parseInt(s.trim());
275         } catch (NumberFormatException e) {
276             logger.log("Bad reply for shock time: " + e.getMessage());
277         }
278 
279         return t;
280     }
281 
282     static class TriggerMessage {
283         public char tag;
284         public long t;
285         public int value;
286         public int count;
287         // TODO: verify the format of the message while parsing it
TriggerMessage(String s)288         TriggerMessage(String s) {
289             String[] parts = s.trim().split("\\s+");
290             tag = parts[0].charAt(0);
291             t = Integer.parseInt(parts[1]);
292             value = Integer.parseInt(parts[2]);
293             count = Integer.parseInt(parts[3]);
294         }
295 
isTriggerString(String s)296         static boolean isTriggerString(String s) {
297             return s.trim().matches("G\\s+[A-Z]\\s+\\d+\\s+\\d+.*");
298         }
299     }
300 
readTriggerMessage(char cmd)301     TriggerMessage readTriggerMessage(char cmd) throws IOException {
302         String response = command(cmd, 'G');
303         return new TriggerMessage(response);
304     }
305 
306 
307     /***********************************************************************************************
308      Trigger Listener
309      A thread that constantly polls the interface for incoming triggers and passes them to the handler
310 
311      */
312 
313     private TriggerListener triggerListener;
314     private Thread triggerListenerThread;
315 
316     abstract static class TriggerHandler {
317         private Handler handler;
318 
TriggerHandler()319         TriggerHandler() {
320             handler = new Handler();
321         }
322 
go(final String s)323         private void go(final String s) {
324             handler.post(new Runnable() {
325                 @Override
326                 public void run() {
327                     onReceiveRaw(s);
328                 }
329             });
330         }
331 
onReceiveRaw(String s)332         void onReceiveRaw(String s) {
333             for (String trigger : s.split("\n")) {
334                 if (TriggerMessage.isTriggerString(trigger)) {
335                     TriggerMessage tmsg = new TriggerMessage(trigger.substring(1).trim());
336                     onReceive(tmsg);
337                 } else {
338                     Log.i(TAG, "Malformed trigger data: " + s);
339                 }
340             }
341         }
342 
onReceive(TriggerMessage tmsg)343         abstract void onReceive(TriggerMessage tmsg);
344     }
345 
346     private TriggerHandler triggerHandler;
347 
setTriggerHandler(TriggerHandler triggerHandler)348     void setTriggerHandler(TriggerHandler triggerHandler) {
349         this.triggerHandler = triggerHandler;
350     }
351 
clearTriggerHandler()352     void clearTriggerHandler() {
353         triggerHandler = null;
354     }
355 
356     private class TriggerListener implements Runnable {
357         static final int BUFF_SIZE = 1024 * 4;
358         public Utils.ListenerState state = Utils.ListenerState.STOPPED;
359         private byte[] buffer = new byte[BUFF_SIZE];
360 
361         @Override
run()362         public void run() {
363             state = Utils.ListenerState.RUNNING;
364             while(isRunning()) {
365                 int ret = connection.blockingRead(buffer);
366                 if (ret > 0 && triggerHandler != null) {
367                     String s = new String(buffer, 0, ret);
368                     Log.i(TAG, "Listener received data: " + s);
369                     if (s.length() > 0) {
370                         triggerHandler.go(s);
371                     }
372                 }
373             }
374             state = Utils.ListenerState.STOPPED;
375         }
376 
isRunning()377         public synchronized boolean isRunning() {
378             return state == Utils.ListenerState.RUNNING;
379         }
380 
isStopped()381         public synchronized boolean isStopped() {
382             return state == Utils.ListenerState.STOPPED;
383         }
384 
stop()385         public synchronized void stop() {
386             state = Utils.ListenerState.STOPPING;
387         }
388     }
389 
isListenerStopped()390     public boolean isListenerStopped() {
391         return triggerListener.isStopped();
392     }
393 
startListener()394     public void startListener() throws IOException {
395         if (!isConnected()) {
396             throw new IOException("Not connected to WALT");
397         }
398         triggerListenerThread = new Thread(triggerListener);
399         logger.log("Starting Listener");
400         triggerListener.state = Utils.ListenerState.STARTING;
401         triggerListenerThread.start();
402     }
403 
stopListener()404     public void stopListener() {
405         // If the trigger listener is already stopped, then it is possible the listener thread is
406         // null. In that case, calling stop() followed by join() will result in a listener object
407         // that is stuck in the STOPPING state.
408         if (triggerListener.isStopped()) {
409             return;
410         }
411         logger.log("Stopping Listener");
412         triggerListener.stop();
413         try {
414             triggerListenerThread.join();
415         } catch (Exception e) {
416             logger.log("Error while stopping Listener: " + e.getMessage());
417         }
418         logger.log("Listener stopped");
419     }
420 
setConnectionStateListener(WaltConnection.ConnectionStateListener connectionStateListener)421     public void setConnectionStateListener(WaltConnection.ConnectionStateListener connectionStateListener) {
422         this.connectionStateListener = connectionStateListener;
423         if (isConnected()) {
424             this.connectionStateListener.onConnect();
425         }
426     }
427 
428 }
429