package org.envirocar.obd.adapter;
import android.util.Base64;
import org.envirocar.core.logging.Logger;
import org.envirocar.obd.commands.PID;
import org.envirocar.obd.commands.PIDSupported;
import org.envirocar.obd.commands.PIDUtil;
import org.envirocar.obd.commands.request.BasicCommand;
import org.envirocar.obd.commands.request.PIDCommand;
import org.envirocar.obd.commands.response.DataResponse;
import org.envirocar.obd.commands.response.ResponseParser;
import org.envirocar.obd.exception.AdapterFailedException;
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.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import rx.Observable;
import rx.Subscriber;
public abstract class SyncAdapter implements OBDAdapter {
private static final Logger LOGGER = Logger.getLogger(SyncAdapter.class.getName());
protected static final long ADAPTER_TRY_PERIOD = 20000;
private static final char COMMAND_SEND_END = '\r';
private static final char COMMAND_RECEIVE_END = '>';
private static final char COMMAND_RECEIVE_SPACE = ' ';
private static final int MAX_ERROR_PER_COMMAND = 5;
private Set<Character> ignoredChars = new HashSet<>(Arrays.asList(COMMAND_RECEIVE_SPACE, COMMAND_SEND_END));
private CommandExecutor commandExecutor;
private ResponseParser parser = new ResponseParser();
private Set<PID> supportedPIDs = new HashSet<>();
private Map<PID, AtomicInteger> failureMap = new HashMap<>();
private List<PIDCommand> requestCommands;
private Queue<PIDCommand> commandRingBuffer = new ArrayDeque<>();
private Queue<PIDSupported> pidSupportedCommands = new ArrayDeque<>(Arrays.asList(new PIDSupported[] {new PIDSupported(), new PIDSupported("20")}));
@Override
public Observable<Boolean> initialize(InputStream is, OutputStream os) {
commandExecutor = new CommandExecutor(is, os, ignoredChars, COMMAND_RECEIVE_END, COMMAND_SEND_END);
/**
* create an observable that tries to verify the
* connection based on response analysis
*/
Observable<Boolean> obs = Observable.create(new Observable.OnSubscribe<Boolean>() {
@Override
public void call(Subscriber<? super Boolean> subscriber) {
try {
boolean analyzedSuccessfully = false;
while (!subscriber.isUnsubscribed()) {
if (analyzedSuccessfully) {
/**
* a successful data connection has been established:
* retrieve the supported PIDs
*/
PIDSupported pid = pidSupportedCommands.poll();
while (pid != null) {
commandExecutor.execute(pid);
byte[] resp = commandExecutor.retrieveLatestResponse();
try {
supportedPIDs.addAll(pid.parsePIDs(resp));
} catch (InvalidCommandResponseException | NoDataReceivedException
| UnmatchedResponseException | AdapterSearchingException e) {
LOGGER.warn(e.getMessage(), e);
}
LOGGER.info("Currently supported PIDs: " + supportedPIDs.toString());
pid = pidSupportedCommands.poll();
}
subscriber.onNext(true);
subscriber.onCompleted();
}
else {
BasicCommand cc = pollNextInitializationCommand();
if (cc == null) {
subscriber.onError(new AdapterFailedException(
"All init commands sent, but could not verify connection"));
subscriber.unsubscribe();
}
LOGGER.info("Sending command in initial phase: "+cc.toString());
//push the command to the output stream
commandExecutor.execute(cc);
//check if the command needs a response (most likely)
if (cc.awaitsResults()) {
try {
Thread.sleep(1000);
byte[] resp = commandExecutor.retrieveLatestResponse();
LOGGER.info("Retrieved initial phase response: "+Base64.encodeToString(resp, Base64.DEFAULT));
analyzedSuccessfully = analyzedSuccessfully | analyzeMetadataResponse(resp, cc);
} catch (InterruptedException e) {
LOGGER.warn(e.getMessage());
}
}
}
}
} catch (IOException | AdapterFailedException e) {
subscriber.onError(e);
subscriber.unsubscribe();
} catch (StreamFinishedException e) {
LOGGER.warn("The stream was closed unexpectedly: "+e.getMessage());
subscriber.onCompleted();
subscriber.unsubscribe();
}
}
});
return obs;
}
@Override
public Observable<DataResponse> observe() {
return Observable.create(new Observable.OnSubscribe<DataResponse>() {
@Override
public void call(Subscriber<? super DataResponse> subscriber) {
LOGGER.info("SyncAdapter.observe().call()");
//prepare all pending data commands
preparePendingCommands();
PIDCommand latestCommand = null;
byte[] bytes = null;
while (!subscriber.isUnsubscribed()) {
try {
latestCommand = pollNextCommand();
LOGGER.debug("Sending command " + (latestCommand != null ? latestCommand.getPid().toString() : "n/a"));
/**
* write the next pending command
*/
if (latestCommand != null) {
commandExecutor.execute(latestCommand);
}
/**
* read the next incoming response
*/
bytes = commandExecutor.retrieveLatestResponse();
DataResponse response = parser.parse(preProcess(bytes));
if (response != null) {
LOGGER.debug("isUnsubscribed? " + subscriber.isUnsubscribed());
subscriber.onNext(response);
}
} catch (IOException e) {
subscriber.onError(e);
subscriber.unsubscribe();
} catch (AdapterFailedException e) {
LOGGER.warn(e.getMessage(), e);
LOGGER.warn(String.format("Sent Command was: %s; Received response was: %s",
latestCommand.getPid().toString(),
Base64.encodeToString(bytes, Base64.DEFAULT)));
subscriber.onError(e);
subscriber.unsubscribe();
} catch (StreamFinishedException e) {
LOGGER.info("Stream finished: "+ e.getMessage());
subscriber.onCompleted();
subscriber.unsubscribe();
} catch (AdapterSearchingException e) {
LOGGER.warn("Adapter still searching: " + e.getMessage());
} catch (NoDataReceivedException e) {
LOGGER.warn("No data received: " + e.getMessage());
increaseFailureCount(latestCommand.getPid());
} catch (InvalidCommandResponseException e) {
LOGGER.warn("Received InvalidCommandResponseException: " + e.getCommand());
increaseFailureCount(PIDUtil.fromString(e.getCommand()));
} catch (UnmatchedResponseException e) {
LOGGER.warn("Unmatched response: " + e.getMessage());
}
}
}
});
}
protected PIDCommand pollNextCommand() throws AdapterFailedException {
if (this.commandRingBuffer.isEmpty()) {
throw new AdapterFailedException("No available commands left in the buffer");
}
PIDCommand cmd = commandRingBuffer.poll();
if (cmd != null) {
if (!checkIsBlacklisted(cmd.getPid())) {
/**
* not blacklisted: add it back as the last element of the ring
*/
commandRingBuffer.offer(cmd);
return cmd;
}
else {
/**
* blacklisted: do not re-add it and return the next candidate
*/
return pollNextCommand();
}
}
return cmd;
}
protected void increaseFailureCount(PID command) {
if (command == null) {
return;
}
if (this.failureMap.containsKey(command)) {
this.failureMap.get(command).getAndIncrement();
}
else {
AtomicInteger ai = new AtomicInteger(1);
this.failureMap.put(command, ai);
}
}
protected List<PIDCommand> defaultCycleCommands() {
if (requestCommands == null) {
requestCommands = new ArrayList<>();
for (PID p: PID.values()) {
addIfSupported(p);
}
}
return requestCommands;
}
protected void addIfSupported(PID pid) {
if (supportedPIDs == null || supportedPIDs.isEmpty()) {
requestCommands.add(PIDUtil.instantiateCommand(pid));
}
else if (supportedPIDs.contains(pid)) {
requestCommands.add(PIDUtil.instantiateCommand(pid));
}
else {
LOGGER.info("PID "+pid.toString()+" not supported. Skipping.");
}
}
private void preparePendingCommands() {
commandRingBuffer = new ArrayDeque<>();
for (PIDCommand cmd : providePendingCommands()) {
if (cmd != null) {
commandRingBuffer.offer(cmd);
}
}
}
private boolean checkIsBlacklisted(PID pid) {
return this.failureMap.containsKey(pid) && this.failureMap.get(pid).get() > MAX_ERROR_PER_COMMAND;
}
@Override
public long getExpectedInitPeriod() {
return ADAPTER_TRY_PERIOD;
}
protected abstract BasicCommand pollNextInitializationCommand();
protected abstract List<PIDCommand> providePendingCommands();
/**
*
* @param response the raw data received
* @param sentCommand the originating command
* @return true if the adapter established a meaningful connection
*/
protected abstract boolean analyzeMetadataResponse(byte[] response, BasicCommand sentCommand) throws AdapterFailedException;
protected abstract byte[] preProcess(byte[] bytes) throws AdapterFailedException;
}