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