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