/*
CanZE
Take a closer look at your ZE car
Copyright (C) 2015 - The CanZE Team
http://canze.fisch.lu
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 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, see <http://www.gnu.org/licenses/>.
*/
package lu.fisch.canze.devices;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import lu.fisch.canze.activities.MainActivity;
import lu.fisch.canze.actors.Field;
import lu.fisch.canze.actors.Frame;
import lu.fisch.canze.actors.Message;
import lu.fisch.canze.actors.VirtualField;
import lu.fisch.canze.bluetooth.BluetoothManager;
import lu.fisch.canze.database.CanzeDataSource;
/**
* This class defines an abstract device. It has to manage the device related
* decoding of the incoming data as well as the data flow to the device or
* whatever is needed to "talk" to it.
*
* Created by robertfisch on 07.09.2015.
*/
public abstract class Device {
public static final int TOUGHNESS_HARD = 0; // hardest reset possible (ie atz)
public static final int TOUGHNESS_MEDIUM = 1; // medium reset (i.e. atws)
public static final int TOUGHNESS_SOFT = 2; // softest reset (i.e atd for ELM)
public static final int TOUGHNESS_NONE = 100; // just clear error status
private final double minIntervalMultiplicator = 1.3;
private final double maxIntervalMultiplicator = 2.5;
double intervalMultiplicator = minIntervalMultiplicator;
/* ----------------------------------------------------------------
* Attributes
\ -------------------------------------------------------------- */
/**
* A device will "monitor" or "request" a given number of fields from
* the connected CAN-device, so this is the list of all fields that
* have to be read and updated.
*/
protected final ArrayList<Field> fields = new ArrayList<>();
/**
* Some fields will be custom, activity based
*/
private ArrayList<Field> activityFieldsScheduled = new ArrayList<>();
private ArrayList<Field> activityFieldsAsFastAsPossible = new ArrayList<>();
/**
* Some other fields will have to be queried anyway,
* such as e.g. the speed --> safe mode driving
*/
private ArrayList<Field> applicationFields = new ArrayList<>();
/**
* The index of the actual field to query.
* Loops over ther "fields" array
*/
//protected int fieldIndex = 0;
private int activityFieldIndex = 0;
private boolean pollerActive = false;
Thread pollerThread;
/**
* lastInitProblem should be filled with a descriptive problem description by the initDevice implementation. In normal operation we don't care
* because a device either initializes or not, but for testing a new device this can be very helpful.
*/
String lastInitProblem = "";
/* ----------------------------------------------------------------
* Abstract methods (to be implemented in each "real" device)
\ -------------------------------------------------------------- */
/**
* A device may need some initialisation before data can be requested.
*/
public void initConnection()
{
MainActivity.debug("Device: initConnection");
if(BluetoothManager.getInstance().isConnected()) {
MainActivity.debug("Device: BT connected");
// make sure we only have one poller task
if (pollerThread == null) {
MainActivity.debug("Device: pollerThread == null");
// post a task to the UI thread
setPollerActive(true);
Runnable r = new Runnable() {
@Override
public void run() {
// if the device has been initialised and we got an answer
if(initDevice(TOUGHNESS_HARD)) {
while (isPollerActive()) {
MainActivity.debug("Device: inside poller thread");
if (applicationFields.size()+activityFieldsScheduled.size()+activityFieldsAsFastAsPossible.size() == 0
|| !BluetoothManager.getInstance().isConnected()) {
MainActivity.debug("Device: sleeping");
try {
if(isPollerActive())
Thread.sleep(5000);
else return;
} catch (Exception e) {
// ignore a sleep exception
}
}
// query a field
else {
if(isPollerActive())
{
MainActivity.debug("Device: Doing next query ...");
queryNextFilter();
}
else return;
}
}
// dereference the poller thread (it i stopped now anyway!)
MainActivity.debug("Device: Poller is done");
pollerThread = null;
}
else
{
MainActivity.debug("Device: no answer from device");
// first check if we have not yet been killed!
if(isPollerActive()) {
MainActivity.debug("Device: --- init failed ---");
// drop the BT connexion and try again
(new Thread(new Runnable() {
@Override
public void run() {
// stop the BT but don't reset the device registered fields
MainActivity.getInstance().stopBluetooth(false);
// reload the BT with filter registration
MainActivity.getInstance().reloadBluetooth(false);
//BluetoothManager.getInstance().connect();
}
})).start();
}
}
}
};
pollerThread = new Thread(r);
// start the thread
pollerThread.start();
}
}
else
{
MainActivity.debug("Device: BT not connected");
if(pollerThread!=null && pollerThread.isAlive())
{
setPollerActive(false);
try {
MainActivity.debug("Device: joining pollerThread");
pollerThread.join();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
// query the device for the next filter
private void queryNextFilter()
{
if (applicationFields.size()+activityFieldsScheduled.size()+activityFieldsAsFastAsPossible.size() > 0)
{
try {
Field field = getNextField();
if(field == null) {
MainActivity.debug("Device: got no next field --> sleeping");
// no next field ---> sleep
try {
Thread.sleep(200);
} catch(Exception e) {
// ignore a sleep exception
}
return;
}
else
{
// long start = Calendar.getInstance().getTimeInMillis();
MainActivity.debug("Device: queryNextFilter: " + field.getSID());
MainActivity.getInstance().dropDebugMessage(field.getSID());
// get the data
Message message = requestFrame(field.getFrame());
// test if we got something
if(!message.isError()) {
//Fields.getInstance().onMessageCompleteEvent(message);
message.onMessageCompleteEvent();
} else {
// one plain retry
message = requestFrame(field.getFrame());
if(!message.isError()) {
message.onMessageCompleteEvent();
} else {
// failed after single retry
// mark underlying fields as uodated to avoid queue clogging
// the will have to get backk to the end of the queue
message.onMessageIncompleteEvent();
// reset if something went wrong ...
// ... but only if we are not asked to stop!
if (BluetoothManager.getInstance().isConnected()) {
MainActivity.debug("Device: something went wrong!");
// we don't want to continue, so we need to stop the poller right now!
// TODO but are we? I don't believe this comment is correct is it?
initDevice(TOUGHNESS_MEDIUM, 2); // toughness = 1, retries = 2
}
}
}
}
}
// if any error occures, reset the fieldIndex
catch (Exception e) {
e.printStackTrace();
}
}
}
private Field getNextField()
{
long referenceTime = Calendar.getInstance().getTimeInMillis();
synchronized (fields) {
if(applicationFields.size()>0) {
/*
// sort the applicationFields
Collections.sort(applicationFields, new Comparator<Field>() {
@Override
public int compare(Field lhs, Field rhs) {
return (int) (lhs.getLastRequest()+lhs.getInterval() - (rhs.getLastRequest()+rhs.getInterval()));
}
});
// get the first field (the one with the smallest lastRequest time
Field field = applicationFields.get(0); */
// get the first field (the one with the smallest lastRequest time
Field field = Collections.min(applicationFields, new Comparator<Field>() {
@Override
public int compare(Field lhs, Field rhs) {
return (int) (lhs.getLastRequest()+lhs.getInterval() - (rhs.getLastRequest()+rhs.getInterval()));
}
});
// return it's index in the global registered field array
if(field.isDue(referenceTime)) {
//MainActivity.debug(Calendar.getInstance().getTimeInMillis()/1000.+" > Chosing: "+field.getSID());
MainActivity.debug("Device: getNextField > applicationFields");
return field;
}
}
// take the next costum field
if(activityFieldsScheduled.size()>0)
{
/*
// sort the activityFields
Collections.sort(activityFieldsScheduled, new Comparator<Field>() {
@Override
public int compare(Field lhs, Field rhs) {
return (int) (lhs.getLastRequest()+lhs.getInterval() - (rhs.getLastRequest()+rhs.getInterval()));
}
});
// get the first field (the one with the smallest lastRequest time
Field field = activityFieldsScheduled.get(0); */
// get the first field (the one with the smallest lastRequest time
Field field = Collections.min(activityFieldsScheduled, new Comparator<Field>() {
@Override
public int compare(Field lhs, Field rhs) {
return (int) (lhs.getLastRequest()+lhs.getInterval() - (rhs.getLastRequest()+rhs.getInterval()));
}
});
// return it's index in the global registered field array
if(field.isDue(referenceTime)) {
//MainActivity.debug(Calendar.getInstance().getTimeInMillis()/1000.+" > Chosing: "+field.getSID());
MainActivity.debug("Device: getNextField > activityFieldsScheduled");
return field;
}
}
if(activityFieldsAsFastAsPossible.size()>0)
{
activityFieldIndex = (activityFieldIndex + 1) % activityFieldsAsFastAsPossible.size();
MainActivity.debug("Device: getNextField > activityFieldsAsFastAsPossible");
return activityFieldsAsFastAsPossible.get(activityFieldIndex);
}
MainActivity.debug("Device: applicationFields & customActivityFields empty? "
+ applicationFields.size() + " / " + activityFieldsScheduled.size()+ " / " + activityFieldsAsFastAsPossible.size());
return null;
}
}
/**
* Ass the CAN bus sends a lot of free frames, the device may want
* to apply a filter. This method should thus register or apply a
* given filter to the hardware.
* @param frameId the ID of the frame to filter for
*/
public abstract void registerFilter(int frameId);
/**
* Method to unregister a filter.
* @param frameId the ID of the frame to no longer filter on
*/
public abstract void unregisterFilter(int frameId);
public void join() throws InterruptedException{
if(pollerThread!=null)
pollerThread.join();
}
/* ----------------------------------------------------------------
* Methods (that will be inherited by any "real" device)
\ -------------------------------------------------------------- */
/**
* This method registers the IDs of all monitored fields.
*/
public void registerFilters()
{
// another thread my also access the list of monitored fields,
// so we need to "protect" it against simultaneous changes.
synchronized (fields) {
for (int i = 0; i < fields.size(); i++) {
registerFilter(fields.get(i).getId());
}
}
}
/**
* This method unregisters all filters from the remote device
*/
public void unregisterFilters()
{
synchronized (fields) {
for (int i = 0; i < fields.size(); i++) {
unregisterFilter(fields.get(i).getId());
}
}
}
/**
* This method clears the list of monitored fields,
* but only the custom ones ...
*/
public void clearFields()
{
MainActivity.debug("Device: clearFields");
synchronized (fields) {
activityFieldsScheduled.clear();
activityFieldsAsFastAsPossible.clear();
fields.clear();
fields.addAll(applicationFields);
//MainActivity.debug("cleared");
// launch the filter clearing asynchronously
(new Thread(new Runnable() {
@Override
public void run() {
unregisterFilters();
}
})).start();
}
}
/**
* A CAN message will trigger updates for all connected fields, meaning
* any field with the same ID and the same responseID will be updated.
* For this reason we don't need to query these fields multiple times
* in one turn.
* @param _field the field to be tested
* @return boolean true if field's frame is already monitored
*/
private boolean containsField(Field _field)
{
for(int i=0; i<fields.size(); i++)
{
Field field = fields.get(i);
if(field.getId()==_field.getId() && field.getResponseId().equals(_field.getResponseId()))
return true;
}
return false;
}
private boolean containsApplicationField(Field _field)
{
for(int i=0; i< applicationFields.size(); i++)
{
Field field = applicationFields.get(i);
if(field.getId()==_field.getId() && field.getResponseId().equals(_field.getResponseId()))
return true;
}
return false;
}
private boolean containsActivityFieldScheduled(Field _field)
{
for(int i=0; i< activityFieldsScheduled.size(); i++)
{
Field field = activityFieldsScheduled.get(i);
if(field.getId()==_field.getId() && field.getResponseId().equals(_field.getResponseId()))
return true;
}
return false;
}
private boolean containsActivityFieldAsFastAsPossible(Field _field)
{
for(int i=0; i< activityFieldsAsFastAsPossible.size(); i++)
{
Field field = activityFieldsAsFastAsPossible.get(i);
if(field.getId()==_field.getId() && field.getResponseId().equals(_field.getResponseId()))
return true;
}
return false;
}
/**
* Method to add a field to the list of monitored field.
* The field is also immediately registered onto the device.
* @param field the field to be added
*/
public void addActivityField(final Field field) {
// ass already present listeners are no being re-registered, do this always
// register it to be saved to the database
field.addListener(CanzeDataSource.getInstance());
if(!field.isVirtual()) {
synchronized (fields) {
if (!containsField(field)) {
// add it to the lists
fields.add(field);
activityFieldsAsFastAsPossible.add(field);
// if the scheduled list constains the same frame id,
// it can be removed there
if (containsActivityFieldScheduled(field))
activityFieldsScheduled.remove(field);
// launch the field registration asynchronously
(new Thread(new Runnable() {
@Override
public void run() {
registerFilter(field.getId());
}
})).start();
}
if (!containsActivityFieldAsFastAsPossible(field)) {
activityFieldsAsFastAsPossible.add(field);
// if the scheduled list constains the same frame id,
// it can be removed there
if (containsActivityFieldScheduled(field))
activityFieldsScheduled.remove(field);
}
}
}
// register real fields on which a virtual field may depend
else
{
VirtualField virtualField = (VirtualField) field;
for (Field realField : virtualField.getFields())
{
addActivityField(realField);
}
}
}
public void addActivityField(final Field field, int interval)
{
// if the interval is 0 or below, the field should be
// added to the list of "as fast as possible" fields.
if (interval <=0 )
{
addActivityField(field);
return;
}
// ass already present listeners are no being re-registered, do this always
// register it to be saved to the database
field.addListener(CanzeDataSource.getInstance());
if(!field.isVirtual()) {
synchronized (fields) {
if (!containsField(field)) {
// add it to the lists
fields.add(field);
activityFieldsScheduled.add(field);
// set the fields query interval
field.setInterval(interval);
// launch the field registration asynchronously
(new Thread(new Runnable() {
@Override
public void run() {
registerFilter(field.getId());
}
})).start();
}
if (!containsActivityFieldScheduled(field)) {
// only add it this field id is not yet on the list of the
// request as fast as possible list.
if (!containsActivityFieldAsFastAsPossible(field))
activityFieldsScheduled.add(field);
// the fields interval will be ignored as the one from the
// applicationFields has priority
} else {
// the smallest intervall is the one to take
if (interval < field.getInterval())
field.setInterval(interval);
}
}
}
// register real fields on which a virtual field may depend
else
{
VirtualField virtualField = (VirtualField) field;
for (Field realField : virtualField.getFields())
{
// increase interval
addActivityField(realField, interval * virtualField.getFields().size());
}
}
}
public void addApplicationField(final Field field, int interval)
{
// ass already present listeners are no being re-registered, do this always
// register it to be saved to the database
field.addListener(CanzeDataSource.getInstance());
if(!field.isVirtual()) {
synchronized (fields) {
if (!containsField(field)) {
// set the fields query interval
field.setInterval(interval);
// add it to the two lists
fields.add(field);
applicationFields.add(field);
// launch the field registration asynchronously
(new Thread(new Runnable() {
@Override
public void run() {
registerFilter(field.getId());
}
})).start();
}
}
}
// register real fields on which a virtual field may depend
else
{
VirtualField virtualField = (VirtualField) field;
for (Field realField : virtualField.getFields())
{
// increase interval
addApplicationField(realField,interval*virtualField.getFields().size());
}
}
}
public void removeApplicationField(final Field field)
{
synchronized (fields) {
// only remove from the custom fields
if(applicationFields.remove(field))
{
// remove it from the database if it is not on the other list
if(!containsActivityFieldScheduled(field)) {
fields.remove(field);
field.setInterval(Integer.MAX_VALUE);
// un-register it ...
field.removeListener(CanzeDataSource.getInstance());
}
// launch the field registration asynchronously
(new Thread(new Runnable() {
@Override
public void run() {
unregisterFilter(field.getId());
}
})).start();
}
}
// remove depenand fields
// ATTENTION; remove the field, despite if it is used by some other VF or not!
//if(field.isVirtual())
//{
// may break something, so please do it manually if really needed!
//}
}
/* ----------------------------------------------------------------
* Methods (that will be inherited by any "real" device)
\ -------------------------------------------------------------- */
public void init(boolean reset) {
// init the connection
initConnection();
if(reset) {
MainActivity.debug("Device: init with reset");
// clean all filters (just to make sure)
clearFields();
// register all filters (if there are any)
registerFilters();
}
else
MainActivity.debug("Device: init");
}
/**
* Stop the poller thread and wait for it to be finished
*/
public void stopAndJoin()
{
MainActivity.debug("Device: stopping poller");
setPollerActive(false);
MainActivity.debug("Device: waiting for poller to be stopped");
try {
if(pollerThread!=null && pollerThread.isAlive()) {
MainActivity.debug("Device: joining thread");
if(pollerThread!=null)
pollerThread.join();
pollerThread=null;
}
else MainActivity.debug("Device: >>>>>>> pollerThread is NULL!!!");
MainActivity.debug("Device: pollerThread joined");
}
catch(Exception e)
{
e.printStackTrace();
}
MainActivity.debug("Device: poller stopped");
}
private boolean isPollerActive() {
return pollerActive;
}
void setPollerActive(boolean pollerActive) {
this.pollerActive = pollerActive;
}
/**
* Request a field from the device depending on the
* type of field.
* @param frame the field to be requested
* @return Message containing the response or an error
*/
public Message requestFrame(Frame frame)
{
Message msg;
if (frame.isIsoTp())
msg = requestIsoTpFrame(frame);
else
msg = requestFreeFrame(frame);
if (msg.isError()) {
MainActivity.debug("Device: request for " + frame.getRID() + " returned error " + msg.getError());
// theory: when the answer is empty, the timeout is to low --> increase it!
// jm: but never beyond 2
if (intervalMultiplicator < maxIntervalMultiplicator) intervalMultiplicator += 0.1;
MainActivity.debug("Device: intervalMultiplicator = " + intervalMultiplicator);
} else {
// theory: when the answer is good, we might recover slowly --> decrease it!
// jm: but never below 1 ----> 2015-12-14 changed 10 1.3
if (intervalMultiplicator > minIntervalMultiplicator) intervalMultiplicator -= 0.01;
MainActivity.debug("Device: intervalMultiplicator = " + intervalMultiplicator);
}
return msg;
}
/**
* Request a free-frame type field from the device
* @param frame The frame requested
* @return Message
*/
public abstract Message requestFreeFrame(Frame frame);
/**
* Request an ISO-TP frame type from the device
* @param frame The frame requested
* @return Message
*/
public abstract Message requestIsoTpFrame(Frame frame);
public abstract boolean initDevice(int toughness);
protected abstract boolean initDevice (int toughness, int retries);
public String getLastInitProblem () {
return lastInitProblem;
}
}