package org.envirocar.obd.adapter.async;
import org.envirocar.core.logging.Logger;
import org.envirocar.obd.adapter.CommandExecutor;
import org.envirocar.obd.adapter.OBDAdapter;
import org.envirocar.obd.adapter.ResponseQuirkWorkaround;
import org.envirocar.obd.commands.request.BasicCommand;
import org.envirocar.obd.commands.response.DataResponse;
import org.envirocar.obd.commands.response.entity.LambdaProbeVoltageResponse;
import org.envirocar.obd.exception.AdapterSearchingException;
import org.envirocar.obd.exception.InvalidCommandResponseException;
import org.envirocar.obd.exception.NoDataReceivedException;
import org.envirocar.obd.exception.StreamFinishedException;
import org.envirocar.obd.exception.UnmatchedResponseException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import rx.Observable;
import rx.Subscriber;
import rx.Subscription;
/**
* Created by matthes on 03.11.15.
*/
public abstract class AsyncAdapter implements OBDAdapter {
private static Logger LOGGER = Logger.getLogger(AsyncAdapter.class);
private static final long DEFAULT_NO_DATA_TIMEOUT = 15000; //*10 for debug
private final char endOfLineOutput;
private final char endOfLineInput;
private CommandExecutor commandExecutor;
private Subscription dataObservable;
private AtomicBoolean quirkDisabled = new AtomicBoolean(false);
public AsyncAdapter(char endOfLineOutput, char endOfLineInput) {
this.endOfLineOutput = endOfLineOutput;
this.endOfLineInput = endOfLineInput;
}
/**
* use this method to disable a CommandExecutor quirk.
* @see {@link #getQuirk()}
*/
public void disableQuirk() {
if (!quirkDisabled.getAndSet(true) && commandExecutor != null) {
commandExecutor.setQuirk(null);
}
}
@Override
public Observable<Boolean> initialize(InputStream is, OutputStream os) {
this.commandExecutor = new CommandExecutor(is, os, Collections.emptySet(), this.endOfLineInput, this.endOfLineOutput);
this.commandExecutor.setQuirk(getQuirk());
/**
*
*/
Observable<Boolean> observable = Observable.create(new Observable.OnSubscribe<Boolean>() {
@Override
public void call(final Subscriber<? super Boolean> subscriber) {
while (!subscriber.isUnsubscribed()) {
/**
* poll the next possible command
*/
BasicCommand cmd = pollNextCommand();
if (cmd != null) {
try {
commandExecutor.execute(cmd);
} catch (IOException e) {
subscriber.onError(e);
subscriber.unsubscribe();
}
}
try {
byte[] response = commandExecutor.retrieveLatestResponse();
processResponse(response);
if (hasEstablishedConnection()) {
subscriber.onNext(true);
subscriber.onCompleted();
}
} catch (IOException e) {
subscriber.onError(e);
subscriber.unsubscribe();
} catch (StreamFinishedException e) {
subscriber.onError(e);
subscriber.unsubscribe();
} catch (InvalidCommandResponseException e) {
LOGGER.warn(e.getMessage(), e);
} catch (NoDataReceivedException e) {
LOGGER.warn(e.getMessage(), e);
} catch (UnmatchedResponseException e) {
LOGGER.warn(e.getMessage(), e);
} catch (AdapterSearchingException e) {
LOGGER.warn(e.getMessage(), e);
}
}
}
});
return observable;
}
protected abstract boolean hasEstablishedConnection();
/**
* an implementation can provide a quirk for response parsing/filtering
*
* @return a quirk that filters a line of raw data
*/
protected abstract ResponseQuirkWorkaround getQuirk();
protected Observable<DataResponse> createDataObservable() {
Observable<DataResponse> dataObservable = Observable.create(new Observable.OnSubscribe<DataResponse>() {
@Override
public void call(Subscriber<? super DataResponse> subscriber) {
while (!subscriber.isUnsubscribed()) {
/**
* poll the next possible command
*/
BasicCommand cmd = pollNextCommand();
if (cmd != null) {
try {
commandExecutor.execute(cmd);
} catch (IOException e) {
subscriber.onError(e);
subscriber.unsubscribe();
}
}
/**
* read the inputstream byte by byte
*/
try {
byte[] bytes = commandExecutor.retrieveLatestResponse();
try {
DataResponse result = processResponse(bytes);
/**
* call our subscriber!
*/
if (result != null) {
subscriber.onNext(result);
if (LOGGER.isEnabled(Logger.DEBUG)) {
if (result instanceof LambdaProbeVoltageResponse) {
LOGGER.debug("Received lambda voltage: "+result);
}
}
}
} catch (AdapterSearchingException e) {
LOGGER.warn("Adapter still searching: " + e.getMessage());
} catch (NoDataReceivedException e) {
LOGGER.warn("No data received: " + e.getMessage());
} catch (InvalidCommandResponseException e) {
LOGGER.warn("InvalidCommandResponseException: " + e.getMessage());
} catch (UnmatchedResponseException e) {
LOGGER.warn("Unmatched response: " + e.getMessage());
}
} catch (IOException e) {
/**
* IOException signals broken connection,
* notify subscriber accordingly
*/
subscriber.onError(e);
return;
} catch (StreamFinishedException e) {
/**
* the stream has ended, notify the subscriber
*/
LOGGER.info("The stream was closed: "+e.getMessage());
subscriber.onCompleted();
return;
}
}
subscriber.onCompleted();
}
}).timeout(DEFAULT_NO_DATA_TIMEOUT, TimeUnit.MILLISECONDS);
return dataObservable;
}
@Override
public Observable<DataResponse> observe() {
return createDataObservable();
}
/**
* an async adapter might want to send commands
* out irregularly or on startup. An implementation
* can provided a command that should be executed using
* this method.
*
* The returned command gets executed after a valid line of
* response has been read.
*
* @return the command or null if there is no pending
*/
protected abstract BasicCommand pollNextCommand();
/**
* Parse a line of response
*
* @param bytes the bytes
* @return a command instace
* @throws InvalidCommandResponseException
* @throws NoDataReceivedException
* @throws UnmatchedResponseException
* @throws AdapterSearchingException
*/
protected abstract DataResponse processResponse(byte[] bytes) throws InvalidCommandResponseException, NoDataReceivedException, UnmatchedResponseException, AdapterSearchingException;
}