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