diff options
Diffstat (limited to 'android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java')
-rw-r--r-- | android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java | 411 |
1 files changed, 411 insertions, 0 deletions
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java new file mode 100644 index 0000000..96ddcfd --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.chromium.latency.walt; + +import android.content.Context; +import android.content.res.Resources; +import android.hardware.usb.UsbDevice; +import android.os.Handler; +import android.util.Log; + +import java.io.IOException; + +/** + * A singleton used as an interface for the physical WALT device. + */ +public class WaltDevice implements WaltConnection.ConnectionStateListener { + + private static final int DEFAULT_DRIFT_LIMIT_US = 1500; + private static final String TAG = "WaltDevice"; + public static final String PROTOCOL_VERSION = "5"; + + // Teensy side commands. Each command is a single char + // Based on #defines section in walt.ino + static final char CMD_PING_DELAYED = 'D'; // Ping with a delay + static final char CMD_RESET = 'F'; // Reset all vars + static final char CMD_SYNC_SEND = 'I'; // Send some digits for clock sync + static final char CMD_PING = 'P'; // Ping with a single byte + static final char CMD_VERSION = 'V'; // Determine WALT's firmware version + static final char CMD_SYNC_READOUT = 'R'; // Read out sync times + static final char CMD_GSHOCK = 'G'; // Send last shock time and watch for another shock. + static final char CMD_TIME_NOW = 'T'; // Current time + static final char CMD_SYNC_ZERO = 'Z'; // Initial zero + static final char CMD_AUTO_SCREEN_ON = 'C'; // Send a message on screen color change + static final char CMD_AUTO_SCREEN_OFF = 'c'; + static final char CMD_SEND_LAST_SCREEN = 'E'; // Send info about last screen color change + static final char CMD_BRIGHTNESS_CURVE = 'U'; // Probe screen for brightness vs time curve + static final char CMD_AUTO_LASER_ON = 'L'; // Send messages on state change of the laser + static final char CMD_AUTO_LASER_OFF = 'l'; + static final char CMD_SEND_LAST_LASER = 'J'; + static final char CMD_AUDIO = 'A'; // Start watching for signal on audio out line + static final char CMD_BEEP = 'B'; // Generate a tone into the mic and send timestamp + static final char CMD_BEEP_STOP = 'S'; // Stop generating tone + static final char CMD_MIDI = 'M'; // Start listening for a MIDI message + static final char CMD_NOTE = 'N'; // Generate a MIDI NoteOn message + + private static final int BYTE_BUFFER_SIZE = 1024 * 4; + private byte[] buffer = new byte[BYTE_BUFFER_SIZE]; + + private Context context; + protected SimpleLogger logger; + private WaltConnection connection; + public RemoteClockInfo clock; + private WaltConnection.ConnectionStateListener connectionStateListener; + + private static final Object LOCK = new Object(); + private static WaltDevice instance; + + public static WaltDevice getInstance(Context context) { + synchronized (LOCK) { + if (instance == null) { + instance = new WaltDevice(context.getApplicationContext()); + } + return instance; + } + } + + private WaltDevice(Context context) { + this.context = context; + triggerListener = new TriggerListener(); + logger = SimpleLogger.getInstance(context); + } + + public void onConnect() { + try { + // TODO: restore + softReset(); + checkVersion(); + syncClock(); + } catch (IOException e) { + logger.log("Unable to communicate with WALT: " + e.getMessage()); + } + + if (connectionStateListener != null) { + connectionStateListener.onConnect(); + } + } + + // Called when disconnecting from WALT + // TODO: restore this, not called from anywhere + public void onDisconnect() { + if (!isListenerStopped()) { + stopListener(); + } + + if (connectionStateListener != null) { + connectionStateListener.onDisconnect(); + } + } + + public void connect() { + if (WaltTcpConnection.probe()) { + logger.log("Using TCP bridge for ChromeOS"); + connection = WaltTcpConnection.getInstance(context); + } else { + // USB connection + logger.log("No TCP bridge detected, using direct USB connection"); + connection = WaltUsbConnection.getInstance(context); + } + connection.setConnectionStateListener(this); + connection.connect(); + } + + public void connect(UsbDevice usbDevice) { + // This happens when apps starts as a result of plugging WALT into USB. In this case we + // receive an intent with a usbDevice + WaltUsbConnection usbConnection = WaltUsbConnection.getInstance(context); + connection = usbConnection; + connection.setConnectionStateListener(this); + usbConnection.connect(usbDevice); + } + + public boolean isConnected() { + return connection.isConnected(); + } + + + public String readOne() throws IOException { + if (!isListenerStopped()) { + throw new IOException("Can't do blocking read while listener is running"); + } + + byte[] buff = new byte[64]; + int ret = connection.blockingRead(buff); + + if (ret < 0) { + throw new IOException("Timed out reading from WALT"); + } + String s = new String(buff, 0, ret); + Log.i(TAG, "readOne() received data: " + s); + return s; + } + + + private String sendReceive(char c) throws IOException { + connection.sendByte(c); + return readOne(); + } + + public void sendAndFlush(char c) { + + try { + connection.sendByte(c); + while(connection.blockingRead(buffer) > 0) { + // flushing all incoming data + } + } catch (Exception e) { + logger.log("Exception in sendAndFlush: " + e.getMessage()); + e.printStackTrace(); + } + } + + public void softReset() { + sendAndFlush(CMD_RESET); + } + + String command(char cmd, char ack) throws IOException { + if (!isListenerStopped()) { + connection.sendByte(cmd); // TODO: check response even if the listener is running + return ""; + } + String response = sendReceive(cmd); + if (!response.startsWith(String.valueOf(ack))) { + throw new IOException("Unexpected response from WALT. Expected \"" + ack + + "\", got \"" + response + "\""); + } + return response.substring(1).trim(); + } + + String command(char cmd) throws IOException { + return command(cmd, flipCase(cmd)); + } + + private char flipCase(char c) { + if (Character.isUpperCase(c)) { + return Character.toLowerCase(c); + } else if (Character.isLowerCase(c)) { + return Character.toUpperCase(c); + } else { + return c; + } + } + + public void checkVersion() throws IOException { + if (!isConnected()) throw new IOException("Not connected to WALT"); + if (!isListenerStopped()) throw new IOException("Listener is running"); + + String s = command(CMD_VERSION); + if (!PROTOCOL_VERSION.equals(s)) { + Resources res = context.getResources(); + throw new IOException(String.format(res.getString(R.string.protocol_version_mismatch), + s, PROTOCOL_VERSION)); + } + } + + public void syncClock() throws IOException { + clock = connection.syncClock(); + } + + // Simple way of syncing clocks. Used for diagnostics. Accuracy of several ms. + public void simpleSyncClock() throws IOException { + byte[] buffer = new byte[1024]; + clock = new RemoteClockInfo(); + clock.baseTime = RemoteClockInfo.microTime(); + String reply = sendReceive(CMD_SYNC_ZERO); + logger.log("Simple sync reply: " + reply); + clock.maxLag = (int) clock.micros(); + logger.log("Synced clocks, the simple way:\n" + clock); + } + + public void checkDrift() { + if (! isConnected()) { + logger.log("ERROR: Not connected, aborting checkDrift()"); + return; + } + connection.updateLag(); + int drift = Math.abs(clock.getMeanLag()); + String msg = String.format("Remote clock delayed between %d and %d us", + clock.minLag, clock.maxLag); + // TODO: Convert the limit to user editable preference + if (drift > DEFAULT_DRIFT_LIMIT_US) { + msg = "WARNING: High clock drift. " + msg; + } + logger.log(msg); + } + + public long readLastShockTime_mock() { + return clock.micros() - 15000; + } + + public long readLastShockTime() { + String s; + try { + s = sendReceive(CMD_GSHOCK); + } catch (IOException e) { + logger.log("Error sending GSHOCK command: " + e.getMessage()); + return -1; + } + Log.i(TAG, "Received S reply: " + s); + long t = 0; + try { + t = Integer.parseInt(s.trim()); + } catch (NumberFormatException e) { + logger.log("Bad reply for shock time: " + e.getMessage()); + } + + return t; + } + + static class TriggerMessage { + public char tag; + public long t; + public int value; + public int count; + // TODO: verify the format of the message while parsing it + TriggerMessage(String s) { + String[] parts = s.trim().split("\\s+"); + tag = parts[0].charAt(0); + t = Integer.parseInt(parts[1]); + value = Integer.parseInt(parts[2]); + count = Integer.parseInt(parts[3]); + } + + static boolean isTriggerString(String s) { + return s.trim().matches("G\\s+[A-Z]\\s+\\d+\\s+\\d+.*"); + } + } + + TriggerMessage readTriggerMessage(char cmd) throws IOException { + String response = command(cmd, 'G'); + return new TriggerMessage(response); + } + + + /*********************************************************************************************** + Trigger Listener + A thread that constantly polls the interface for incoming triggers and passes them to the handler + + */ + + private TriggerListener triggerListener; + private Thread triggerListenerThread; + + abstract static class TriggerHandler { + private Handler handler; + + TriggerHandler() { + handler = new Handler(); + } + + private void go(final String s) { + handler.post(new Runnable() { + @Override + public void run() { + onReceiveRaw(s); + } + }); + } + + void onReceiveRaw(String s) { + if (TriggerMessage.isTriggerString(s)) { + TriggerMessage tmsg = new TriggerMessage(s.substring(1).trim()); + onReceive(tmsg); + } else { + Log.i(TAG, "Malformed trigger data: " + s); + } + } + + abstract void onReceive(TriggerMessage tmsg); + } + + private TriggerHandler triggerHandler; + + void setTriggerHandler(TriggerHandler triggerHandler) { + this.triggerHandler = triggerHandler; + } + + void clearTriggerHandler() { + triggerHandler = null; + } + + private class TriggerListener implements Runnable { + static final int BUFF_SIZE = 1024 * 4; + public Utils.ListenerState state = Utils.ListenerState.STOPPED; + private byte[] buffer = new byte[BUFF_SIZE]; + + @Override + public void run() { + state = Utils.ListenerState.RUNNING; + while(isRunning()) { + int ret = connection.blockingRead(buffer); + if (ret > 0 && triggerHandler != null) { + String s = new String(buffer, 0, ret); + Log.i(TAG, "Listener received data: " + s); + if (s.length() > 0) { + triggerHandler.go(s); + } + } + } + state = Utils.ListenerState.STOPPED; + } + + public synchronized boolean isRunning() { + return state == Utils.ListenerState.RUNNING; + } + + public synchronized boolean isStopped() { + return state == Utils.ListenerState.STOPPED; + } + + public synchronized void stop() { + state = Utils.ListenerState.STOPPING; + } + } + + public boolean isListenerStopped() { + return triggerListener.isStopped(); + } + + public void startListener() throws IOException { + if (!isConnected()) { + throw new IOException("Not connected to WALT"); + } + triggerListenerThread = new Thread(triggerListener); + logger.log("Starting Listener"); + triggerListener.state = Utils.ListenerState.STARTING; + triggerListenerThread.start(); + } + + public void stopListener() { + logger.log("Stopping Listener"); + triggerListener.stop(); + try { + triggerListenerThread.join(); + } catch (Exception e) { + logger.log("Error while stopping Listener: " + e.getMessage()); + } + logger.log("Listener stopped"); + } + + public void setConnectionStateListener(WaltConnection.ConnectionStateListener connectionStateListener) { + this.connectionStateListener = connectionStateListener; + if (isConnected()) { + this.connectionStateListener.onConnect(); + } + } + +} |