package com.openxc.sources; import java.io.IOException; import java.util.Timer; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import android.content.Context; import android.util.Log; import com.openxc.messages.SerializationException; import com.openxc.messages.VehicleMessage; import com.openxc.messages.streamers.BinaryStreamer; import com.openxc.messages.streamers.JsonStreamer; import com.openxc.messages.streamers.VehicleMessageStreamer; import com.openxc.sinks.DataSinkException; /** * Common functionality for data sources that read a stream of newline-separated * messages in a separate thread from the main activity. */ public abstract class BytestreamDataSource extends ContextualVehicleDataSource implements Runnable { private final static int READ_BATCH_SIZE = 512; private static final int MAX_FAST_RECONNECTION_ATTEMPTS = 6; protected static final int RECONNECTION_ATTEMPT_WAIT_TIME_S = 10; protected static final int SLOW_RECONNECTION_ATTEMPT_WAIT_TIME_S = 60; private AtomicBoolean mRunning = new AtomicBoolean(false); private int mReconnectionAttempts; protected final ReadWriteLock mConnectionLock = new ReentrantReadWriteLock(); protected final Condition mDeviceChanged = mConnectionLock.writeLock().newCondition(); private Thread mThread; private Timer mTimer; private BytestreamConnectingTask mConnectionCheckTask; private VehicleMessageStreamer mStreamHandler = null; private boolean mFastPolling = true; public BytestreamDataSource(SourceCallback callback, Context context) { super(callback, context); } public void start() { if(mRunning.compareAndSet(false, true)) { Log.d(getTag(), "Starting " + getTag() + " source"); mThread = new Thread(this); mThread.start(); } } @Override public void stop() { if(mRunning.compareAndSet(true, false)) { Log.d(getTag(), "Stopping " + getTag() + " source"); mThread.interrupt(); } } protected void setFastPolling(boolean enabled) { mReconnectionAttempts = 0; if(enabled) { resetConnectionAttempts(0, RECONNECTION_ATTEMPT_WAIT_TIME_S); } else if(mFastPolling) { resetConnectionAttempts(SLOW_RECONNECTION_ATTEMPT_WAIT_TIME_S, SLOW_RECONNECTION_ATTEMPT_WAIT_TIME_S); } mFastPolling = enabled; } /** * If not already connected to the data source, initiate the connection and * block until ready to be read. * * You must have the mConnectionLock locked before calling this * function. * * @throws InterruptedException if the interrupted while blocked -- probably * shutting down. */ protected void waitForConnection() throws InterruptedException { if(!isConnected() && mConnectionCheckTask == null) { setFastPolling(true); } while(isRunning() && !isConnected()) { ++mReconnectionAttempts; mConnectionLock.writeLock().lock(); try { mDeviceChanged.await(); if(mReconnectionAttempts == MAX_FAST_RECONNECTION_ATTEMPTS) { Log.d(this.getClass().getSimpleName(), "Unable to connect after " + MAX_FAST_RECONNECTION_ATTEMPTS + " attempts, slowing down attempts to every " + SLOW_RECONNECTION_ATTEMPT_WAIT_TIME_S + " seconds"); setFastPolling(false); } } finally { mConnectionLock.writeLock().unlock(); } } mReconnectionAttempts = 0; } private void resetConnectionAttempts(long delay, long period) { stopConnectionAttempts(); mConnectionCheckTask = new BytestreamConnectingTask(this); mTimer = new Timer(); mTimer.schedule(mConnectionCheckTask, delay * 1000, period * 1000); } protected void stopConnectionAttempts() { if(mTimer != null) { mTimer.cancel(); } mReconnectionAttempts = 0; } @Override public void run() { while(isRunning()) { try { waitForConnection(); } catch(InterruptedException e) { Log.i(getTag(), "Interrupted while waiting for connection - stopping the source"); stop(); break; } int received; byte[] bytes = new byte[READ_BATCH_SIZE]; try { received = read(bytes); } catch(IOException e) { Log.e(getTag(), "Unable to read response", e); disconnect(); continue; } if(received == -1) { Log.e(getTag(), "Error on read - returned -1"); disconnect(); continue; } if(received > 0) { synchronized(this) { if(mStreamHandler == null) { if(JsonStreamer.containsJson(new String(bytes))) { mStreamHandler = new JsonStreamer(); Log.i(getTag(), "Source is sending JSON"); } else { mStreamHandler = new BinaryStreamer(); Log.i(getTag(), "Source is sending protocol buffers"); } } } mStreamHandler.receive(bytes, received); VehicleMessage message; while((message = mStreamHandler.parseNextMessage()) != null) { handleMessage(message); } } } disconnect(); Log.d(getTag(), "Stopped " + getTag()); } public void receive(VehicleMessage command) throws DataSinkException { if(isConnected()) { VehicleMessageStreamer streamer; synchronized(this) { streamer = mStreamHandler; if(streamer == null) { // See https://github.com/openxc/openxc-android/issues/181 streamer = new JsonStreamer(); Log.i(getTag(), "Payload format unknown, guessing JSON"); } } try { if(!write(streamer.serializeForStream(command))) { throw new DataSinkException("Unable to send command"); } } catch(SerializationException e) { throw new DataSinkException( "Unable to serialize command for sending", e); } } else { throw new DataSinkException("Not connected"); } } @Override public boolean isConnected() { return isRunning(); } /** * Must have the connection lock before calling this function */ @Override protected void disconnected() { mDeviceChanged.signal(); super.disconnected(); } /** * Must have the connection lock before calling this function */ @Override protected void connected() { mDeviceChanged.signal(); super.connected(); } /** * Returns true if this source should be running, or if it should die. * * This is different than isConnected - they just happen to return the same * thing in this base data source. */ protected boolean isRunning() { return mRunning.get(); } /** * Read data from the source into the given array. * * No more than bytes.length bytes will be read, and there is no guarantee * that any bytes will be read at all. * * @param bytes the destination array for bytes from the data source. * @return the number of bytes that were actually copied into bytes. * @throws IOException if the source is unexpectedly closed or returns an * error. */ protected abstract int read(byte[] bytes) throws IOException; protected abstract boolean write(byte[] bytes); /** * Perform any cleanup necessary to disconnect from the interface. */ protected abstract void disconnect(); /** Initiate a connection to the vehicle interface. */ protected abstract void connect() throws DataSourceException; }