/* * enviroCar 2013 * Copyright (C) 2013 * Martin Dueren, Jakob Moellers, Gerald Pape, Christopher Stephan * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ package org.envirocar.obd; import com.google.common.base.Preconditions; import com.squareup.otto.Bus; import org.envirocar.core.entity.Measurement; import org.envirocar.core.logging.Logger; import org.envirocar.obd.adapter.AposW3Adapter; import org.envirocar.obd.adapter.CarTrendAdapter; import org.envirocar.obd.adapter.ELM327Adapter; import org.envirocar.obd.adapter.OBDAdapter; import org.envirocar.obd.adapter.async.DriveDeckSportAdapter; import org.envirocar.obd.bluetooth.BluetoothSocketWrapper; import org.envirocar.obd.commands.PID; import org.envirocar.obd.commands.PIDUtil; import org.envirocar.obd.commands.response.DataResponse; import org.envirocar.obd.events.PropertyKeyEvent; import org.envirocar.obd.events.RPMUpdateEvent; import org.envirocar.obd.events.SpeedUpdateEvent; import org.envirocar.obd.exception.AllAdaptersFailedException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayDeque; import java.util.Queue; import java.util.concurrent.TimeUnit; import rx.Scheduler; import rx.Subscriber; import rx.Subscription; import rx.schedulers.Schedulers; /** * this is the main class for interacting with a OBD-II adapter. * It takes {@link InputStream} and {@link OutputStream} objects * to do the actual raw communication. The {@link ConnectionListener} will get informed on * certain changes in the connection state. * * @author matthes rieke */ public class OBDController { private static final Logger LOG = Logger.getLogger(OBDController.class); public static final long MAX_NODATA_TIME = 10000; private Subscription initSubscription; private Subscription dataSubscription; private Queue<OBDAdapter> adapterCandidates = new ArrayDeque<>(); private OBDAdapter obdAdapter; private InputStream inputStream; private OutputStream outputStream; private ConnectionListener connectionListener; private String deviceName; private boolean userRequestedStop = false; private Bus eventBus; private Scheduler.Worker eventBusWorker; /** * Default Constructor. * * @param bluetoothSocketWrapper * @param cl * @param bus */ public OBDController(BluetoothSocketWrapper bluetoothSocketWrapper, ConnectionListener cl, Bus bus) throws IOException { this(bluetoothSocketWrapper.getInputStream(), bluetoothSocketWrapper.getOutputStream(), bluetoothSocketWrapper.getRemoteDeviceName(), cl, bus); } /** * Init the OBD control layer with the streams and listeners to be used. * * @param in the inputStream of the connection * @param out the outputStream of the connection * @param cl the connection listener which receives connection state changes */ public OBDController(InputStream in, OutputStream out, String deviceName, ConnectionListener cl, Bus bus) { this.inputStream = Preconditions.checkNotNull(in); this.outputStream = Preconditions.checkNotNull(out); this.connectionListener = Preconditions.checkNotNull(cl); this.deviceName = Preconditions.checkNotNull(deviceName); setupAdapterCandidates(); startPreferredAdapter(); this.eventBus = bus; if (this.eventBus != null) { this.eventBusWorker = Schedulers.io().createWorker(); } } /** * setup the list of available Adapter implementations */ private void setupAdapterCandidates() { adapterCandidates.clear(); adapterCandidates.offer(new ELM327Adapter()); adapterCandidates.offer(new CarTrendAdapter()); adapterCandidates.offer(new AposW3Adapter()); adapterCandidates.offer(new DriveDeckSportAdapter()); } /** * start the preferred adapter, determined by the device name */ private void startPreferredAdapter() { for (OBDAdapter ac : adapterCandidates) { if (ac.supportsDevice(this.deviceName)) { this.obdAdapter = ac; break; } } if (this.obdAdapter == null) { //poll the first instead this.obdAdapter = adapterCandidates.poll(); } else { //remove the preferred from the queue so it is not used again this.adapterCandidates.remove(this.obdAdapter); } LOG.info("Using " + this.obdAdapter.getClass().getSimpleName() + " connector as the " + "preferred adapter."); startInitialization(false); } /** * select the next adapter candidates from the list of implementations * * @throws AllAdaptersFailedException if the list has reached its end */ private void selectNextAdapter() throws AllAdaptersFailedException { this.obdAdapter = adapterCandidates.poll(); if (this.obdAdapter == null) { throw new AllAdaptersFailedException("All candidate adapters failed"); } } /** * start the init method of the adapter. This is used * to bootstrap and verify the connection of the adapter * with the ECU. * <p> * The init times out after a pre-defined period. */ private void startInitialization(boolean alreadyTried) { // start the observable and subscribe to it this.initSubscription = this.obdAdapter.initialize(this.inputStream, this.outputStream) .subscribeOn(Schedulers.io()) .observeOn(OBDSchedulers.scheduler()) .timeout(this.obdAdapter.getExpectedInitPeriod(), TimeUnit.MILLISECONDS) .subscribe(getInitSubscriber(alreadyTried)); } private Subscriber<Boolean> getInitSubscriber(boolean alreadyTried) { return new Subscriber<Boolean>() { @Override public void onCompleted() { LOG.info("Connecting has been initialized!"); } @Override public void onError(Throwable e) { LOG.warn("Adapter failed: " + obdAdapter.getClass().getSimpleName(), e); try { this.unsubscribe(); if (obdAdapter.hasCertifiedConnection()) { if (!alreadyTried) { // one retry if it was verified! startInitialization(true); } else { throw new AllAdaptersFailedException( "Adapter verified a connection but could not establishe data: " + obdAdapter.getClass().getSimpleName()); } } else { selectNextAdapter(); // try the selected adapter startInitialization(false); } } catch (AllAdaptersFailedException e1) { LOG.warn("All Adapters failed", e1); connectionListener.onAllAdaptersFailed(); //TODO implement equivalent notification method: //dataListener.shutdown(); } } @Override public void onNext(Boolean b) { LOG.info("Connection verified - starting data collection"); //unsubscribe, otherwise we will get a timeout this.unsubscribe(); startCollectingData(); //TODO implement equivalent notification method: //dataListener.onConnected(deviceName); } }; } /** * start the actual collection of data. * <p> * the collection times out after a pre-defined period when no * new data has arrived. */ private void startCollectingData() { LOG.info("OBDController.startCollectingData()"); //inform the listener about the successful conn this.connectionListener.onConnectionVerified(); // start the observable with a timeout this.dataSubscription = this.obdAdapter.observe() .subscribeOn(Schedulers.io()) .observeOn(OBDSchedulers.scheduler()) .timeout(MAX_NODATA_TIME, TimeUnit.MILLISECONDS) .subscribe(getCollectingDataSubscriber()); } private Subscriber<DataResponse> getCollectingDataSubscriber() { return new Subscriber<DataResponse>() { @Override public void onCompleted() { LOG.info("onCompleted(): data collection"); //TODO implement equivalent notification method: //dataListener.shutdown(); } @Override public void onError(Throwable e) { LOG.warn("onError() received", e); // check if this is a demanded stop: still this can lead to any kind of Exception if (userRequestedStop) { //TODO implement equivalent notification method: //dataListener.shutdown(); } connectionListener.onAllAdaptersFailed(); this.unsubscribe(); } @Override public void onNext(DataResponse dataResponse) { pushToEventBus(dataResponse); } }; } private void pushToEventBus(DataResponse dataResponse) { eventBusWorker.schedule(() -> { PropertyKeyEvent[] pkes = createEventsFromDataResponse(dataResponse); for (PropertyKeyEvent pke : pkes) { eventBus.post(pke); } PID pid = dataResponse.getPid(); if (pid == PID.SPEED) { eventBus.post(new SpeedUpdateEvent(dataResponse.getValue().intValue())); } else if (pid == PID.RPM) { eventBus.post(new RPMUpdateEvent(dataResponse.getValue().intValue())); } }); } protected PropertyKeyEvent[] createEventsFromDataResponse(DataResponse dataResponse) { PID pid = dataResponse.getPid(); switch (pid) { // case FUEL_SYSTEM_STATUS: case CALCULATED_ENGINE_LOAD: case SHORT_TERM_FUEL_TRIM_BANK_1: case LONG_TERM_FUEL_TRIM_BANK_1: case FUEL_PRESSURE: case INTAKE_MAP: case RPM: case SPEED: case INTAKE_AIR_TEMP: case MAF: case TPS: return new PropertyKeyEvent[]{ new PropertyKeyEvent(PIDUtil.toPropertyKey(pid), dataResponse.getValue(), dataResponse.getTimestamp()) }; case O2_LAMBDA_PROBE_1_VOLTAGE: case O2_LAMBDA_PROBE_2_VOLTAGE: case O2_LAMBDA_PROBE_3_VOLTAGE: case O2_LAMBDA_PROBE_4_VOLTAGE: case O2_LAMBDA_PROBE_5_VOLTAGE: case O2_LAMBDA_PROBE_6_VOLTAGE: case O2_LAMBDA_PROBE_7_VOLTAGE: case O2_LAMBDA_PROBE_8_VOLTAGE: return new PropertyKeyEvent[]{ new PropertyKeyEvent(Measurement.PropertyKey.LAMBDA_VOLTAGE_ER, dataResponse.getCompositeValues()[0], dataResponse.getTimestamp()), new PropertyKeyEvent(Measurement.PropertyKey.LAMBDA_VOLTAGE, dataResponse.getCompositeValues()[1], dataResponse.getTimestamp()) }; case O2_LAMBDA_PROBE_1_CURRENT: case O2_LAMBDA_PROBE_2_CURRENT: case O2_LAMBDA_PROBE_3_CURRENT: case O2_LAMBDA_PROBE_4_CURRENT: case O2_LAMBDA_PROBE_5_CURRENT: case O2_LAMBDA_PROBE_6_CURRENT: case O2_LAMBDA_PROBE_7_CURRENT: case O2_LAMBDA_PROBE_8_CURRENT: return new PropertyKeyEvent[]{ new PropertyKeyEvent(Measurement.PropertyKey.LAMBDA_CURRENT_ER, dataResponse.getCompositeValues()[0], dataResponse.getTimestamp()), new PropertyKeyEvent(Measurement.PropertyKey.LAMBDA_CURRENT, dataResponse.getCompositeValues()[1], dataResponse.getTimestamp()) }; } return new PropertyKeyEvent[0]; } /** * Shutdown the controller. this removes all pending commands. * This object is no longer executable, a new instance has to * be created. * <p> * Only use this if the stop is from high-level (e.g. user request) * and NOT on any kind of exception */ public void shutdown() { LOG.info("OBDController.shutdown()"); /** * save that this is a stop on demand */ userRequestedStop = true; if (this.initSubscription != null) { this.initSubscription.unsubscribe(); } if (this.dataSubscription != null) { this.dataSubscription.unsubscribe(); } } }