Home | History | Annotate | Download | only in walt
      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 
     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 
     81     private WaltDevice(Context context) {
     82         this.context = context;
     83         triggerListener = new TriggerListener();
     84         logger = SimpleLogger.getInstance(context);
     85     }
     86 
     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
    104     public void onDisconnect() {
    105         if (!isListenerStopped()) {
    106             stopListener();
    107         }
    108 
    109         if (connectionStateListener != null) {
    110             connectionStateListener.onDisconnect();
    111         }
    112     }
    113 
    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 
    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 
    136     public boolean isConnected() {
    137         return connection.isConnected();
    138     }
    139 
    140 
    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 
    158     private String sendReceive(char c) throws IOException {
    159         synchronized (connection) {
    160             connection.sendByte(c);
    161             return readOne();
    162         }
    163     }
    164 
    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 
    180     public void softReset() {
    181         sendAndFlush(CMD_RESET);
    182     }
    183 
    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 
    198     String command(char cmd) throws IOException {
    199         return command(cmd, flipCase(cmd));
    200     }
    201 
    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 
    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 
    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.
    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 
    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 
    259     public long readLastShockTime_mock() {
    260         return clock.micros() - 15000;
    261     }
    262 
    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
    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 
    296         static boolean isTriggerString(String s) {
    297             return s.trim().matches("G\\s+[A-Z]\\s+\\d+\\s+\\d+.*");
    298         }
    299     }
    300 
    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 
    319         TriggerHandler() {
    320             handler = new Handler();
    321         }
    322 
    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 
    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 
    343         abstract void onReceive(TriggerMessage tmsg);
    344     }
    345 
    346     private TriggerHandler triggerHandler;
    347 
    348     void setTriggerHandler(TriggerHandler triggerHandler) {
    349         this.triggerHandler = triggerHandler;
    350     }
    351 
    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
    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 
    377         public synchronized boolean isRunning() {
    378             return state == Utils.ListenerState.RUNNING;
    379         }
    380 
    381         public synchronized boolean isStopped() {
    382             return state == Utils.ListenerState.STOPPED;
    383         }
    384 
    385         public synchronized void stop() {
    386             state = Utils.ListenerState.STOPPING;
    387         }
    388     }
    389 
    390     public boolean isListenerStopped() {
    391         return triggerListener.isStopped();
    392     }
    393 
    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 
    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 
    421     public void setConnectionStateListener(WaltConnection.ConnectionStateListener connectionStateListener) {
    422         this.connectionStateListener = connectionStateListener;
    423         if (isConnected()) {
    424             this.connectionStateListener.onConnect();
    425         }
    426     }
    427 
    428 }
    429