blob: 96ddcfd8d98d0b0439d4b147c075e42c8f94afd2 [file] [log] [blame]
/*
* 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();
}
}
}