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
17package org.chromium.latency.walt;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.hardware.usb.UsbDevice;
22import android.os.Handler;
23import android.util.Log;
24
25import java.io.IOException;
26
27/**
28 * A singleton used as an interface for the physical WALT device.
29 */
30public 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