aboutsummaryrefslogtreecommitdiff
path: root/android/WALT/app/src/main/java/org/chromium/latency/walt/WaltDevice.java
diff options
context:
space:
mode:
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.java411
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();
+ }
+ }
+
+}