/* * 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 { synchronized (connection) { connection.sendByte(c); return readOne(); } } public void sendAndFlush(char c) { try { synchronized (connection) { 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 + "\""); } // Trim out the ack 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(); if (clock == null) { // updateLag() will have logged a message if we get here return; } 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) { for (String trigger : s.split("\n")) { if (TriggerMessage.isTriggerString(trigger)) { TriggerMessage tmsg = new TriggerMessage(trigger.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() { // If the trigger listener is already stopped, then it is possible the listener thread is // null. In that case, calling stop() followed by join() will result in a listener object // that is stuck in the STOPPING state. if (triggerListener.isStopped()) { return; } 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(); } } }