/**
* This file is part of ShareNav.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
* Copyright (c) 2008 Kai Krueger apmonkey at users dot sourceforge dot net
* See COPYING
*/
package net.sharenav.gps.location;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Timer;
import java.util.TimerTask;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import net.sharenav.sharenav.data.Configuration;
import net.sharenav.sharenav.ui.ShareNav;
import net.sharenav.util.Logger;
import de.enough.polish.util.Locale;
/**
* This class shares the functionality to read from the Bluetooth GPS receiver
* and handles common functionality such as dealing with receiver quality and
* lost connections.
*
* The protocol specific decoding is handled in the abstract process() function
* of subclasses.
*/
public abstract class BtReceiverInput implements Runnable, LocationMsgProducer {
private static final Logger logger = Logger.getInstance(NmeaInput.class,
Logger.TRACE);
protected InputStream btGpsInputStream;
private OutputStream btGpsOutputStream;
private StreamConnection conn;
protected OutputStream rawDataLogger;
protected Thread processorThread;
protected LocationMsgReceiverList receiverList;
protected boolean closed = false;
protected byte connectQuality = 100;
protected int bytesReceived = 0;
protected int[] connectError = new int[LocationMsgReceiver.SIRF_FAIL_COUNT];
protected String message;
protected int msgsReceived = 1;
public BtReceiverInput() {
this.receiverList = new LocationMsgReceiverList();
}
protected class KeepAliveTimer extends TimerTask {
public void run() {
if (btGpsOutputStream != null) {
try {
logger.debug("Writing bogus keep-alive");
btGpsOutputStream.write(0);
} catch (IOException e) {
logger.info("Closing keep alive timer");
this.cancel();
} catch (IllegalArgumentException iae) {
logger.silentexception("KeepAliveTimer went wrong", iae);
}
}
}
}
public boolean init(LocationMsgReceiver receiver) {
if (receiver != null) {
this.receiverList.addReceiver(receiver);
}
//#debug info
logger.info("Connect to "+Configuration.getBtUrl());
if (! openBtConnection(Configuration.getBtUrl())){
this.receiverList.locationDecoderEnd();
return false;
}
this.receiverList.receiveMessage(Locale.get("btreceiverinput.BTconnected")/*BT Connected*/);
processorThread = new Thread(this, "Bluetooth Receiver Decoder");
processorThread.setPriority(Thread.MAX_PRIORITY);
processorThread.start();
/**
* There is at least one, perhaps more Bt GPS receivers, that seem to
* kill the Bluetooth connection if we don't send it something for some
* reason. Perhaps due to poor power management? We don't have anything
* to send, so send an arbitrary 0.
*/
if (Configuration.getBtKeepAlive()) {
TimerTask tt = new KeepAliveTimer();
logger.info("Setting keep alive timer: " + tt);
ShareNav.getTimer().schedule(tt, 1000, 1000);
}
return true;
}
public boolean activate(LocationMsgReceiver receiver) {
// FIXME move activation code (code to enable continuos location feed) here
return true;
}
public boolean deactivate(LocationMsgReceiver receiver) {
return true;
}
abstract protected void process() throws IOException;
public void run() {
receiverList.receiveMessage(Locale.get("btreceiverinput.StartBTreceiver")/*Start Bt GPS receiver*/);
// Eat the buffer content
try {
try {
byte[] buf = new byte[512];
while (btGpsInputStream.available() > 0) {
int received = btGpsInputStream.read(buf);
bytesReceived += received;
if (rawDataLogger != null) {
rawDataLogger.write(buf, 0, received);
rawDataLogger.flush();
}
}
// #debug debug
logger.debug("Erased " + bytesReceived + " bytes");
bytesReceived = 100;
} catch (IOException e1) {
receiverList.receiveMessage(Locale.get("btreceiverinput.BTClosing2")/*Closing: */ + e1.getMessage());
close(Locale.get("btreceiverinput.BTClosing2")/*Closing: */ + e1.getMessage());
}
byte timeCounter = 21;
while (!closed) {
//#debug debug
logger.debug("Bt receiver thread looped");
try {
timeCounter++;
// 20 * 250 ms = 5 s
if (timeCounter > 20) {
timeCounter = 0;
if (connectQuality > 100) {
connectQuality = 100;
}
if (connectQuality < 0) {
connectQuality = 0;
}
receiverList.receiveStatistics(connectError, connectQuality);
// Watchdog: if no bytes received in 10 sec then exit thread
if (bytesReceived == 0) {
throw new IOException(Locale.get("btreceiverinput.BTNoData")/*No Data from GPS*/);
} else {
bytesReceived = 0;
}
}
process();
} catch (IOException e) {
/**
* The bluetooth connection seems to have died, try and
* reconnect. If the reconnect was successful the reconnect
* function will call the init function, which will create a
* new thread. In that case we simply exit this old thread.
* If the reconnect was unsuccessful then we close the
* connection and give an error message.
*/
logger.info("Failed to read from GPS trying to reconnect: "
+ e.getMessage());
receiverList.receiveStatus(LocationMsgReceiver.STATUS_RECONNECT, 0);
if (!autoReconnectBtConnection()) {
logger.info("GPS bluethooth could not reconnect");
receiverList.receiveMessage(Locale.get("btreceiverinput.BTClosing2")/*Closing: */ + e.getMessage());
close(Locale.get("btreceiverinput.BTAutoClose")/*Closed: */ + e.getMessage());
} else {
logger.info("GPS bluetooth reconnect was successful");
return;
}
}
if (!closed) {
try {
synchronized (this) {
connectError[LocationMsgReceiver.SIRF_FAIL_MSG_INTERUPTED]++;
wait(250);
}
} catch (InterruptedException e) {
// Nothing to do in this case
}
}
}
} catch (OutOfMemoryError oome) {
closed = true;
logger.fatal(Locale.get("btreceiverinput.ExOOM")/*BtReceiverInput thread ran out of memory: */
+ oome.getMessage());
oome.printStackTrace();
} catch (Exception e) {
closed = true;
logger.fatal(Locale.get("btreceiverinput.ExCrashUnexp")/*BtReceiverInput thread crashed unexpectedly: */
+ e.getMessage());
e.printStackTrace();
} finally {
if (closed) {
logger.info("Finished LocationProducer thread, closing bluetooth");
closeBtConnection();
if (message == null) {
receiverList.locationDecoderEnd();
} else {
receiverList.locationDecoderEnd(message);
}
} else {
/**
* Don't need to do anything here.
* This is the case when we are auto-reconnecting
* and starting up a new bluetooth processing thread
*/
}
}
}
/*
* (non-Javadoc)
*
* @see net.sharenav.gps.nmea.LocationMsgProducer#close()
*/
public void close() {
logger.info("Location producer closing");
closed = true;
if (processorThread != null) {
processorThread.interrupt();
}
}
public void close(String message) {
this.message = message;
close();
}
public void enableRawLogging(OutputStream os) {
rawDataLogger = os;
}
public void disableRawLogging() {
if (rawDataLogger != null) {
try {
rawDataLogger.close();
} catch (IOException e) {
logger.exception(Locale.get("btreceiverinput.ExCloserawGPS")/*Could not close raw GPS logger*/, e);
}
rawDataLogger = null;
}
}
public void triggerLastKnownPositionUpdate() {
}
public void triggerPositionUpdate() {
//FIXME make a proper interface for passing fix age information instead of accessing trace variable directly
//tr.gpsRecenterInvalid = true;
//tr.gpsRecenterStale = true;
//locationUpdated(locationProvider, LocationProvider.getLastKnownLocation());
}
public void addLocationMsgReceiver(LocationMsgReceiver receiver) {
receiverList.addReceiver(receiver);
}
public boolean removeLocationMsgReceiver(LocationMsgReceiver receiver) {
return receiverList.removeReceiver(receiver);
}
private synchronized boolean openBtConnection(String url){
if (btGpsInputStream != null){
return true;
}
if (url == null) {
return false;
}
try {
logger.info("Connector.open()");
conn = (StreamConnection) Connector.open(url);
logger.info("conn.openInputStream()");
btGpsInputStream = conn.openInputStream();
/**
* There is at least one, perhaps more BT gps receivers, that
* seem to kill the bluetooth connection if we don't send it
* something for some reason. Perhaps due to poor powermanagment?
* We don't have anything to send, so send an arbitrary 0.
*/
if (Configuration.getBtKeepAlive()) {
btGpsOutputStream = conn.openOutputStream();
}
if (Configuration.getLocationProvider() == Configuration.LOCATIONPROVIDER_GPSD) {
btGpsOutputStream = conn.openOutputStream();
btGpsOutputStream.write("?WATCH={\"enable\":true,\"nmea\":true}\r\n".getBytes("ISO-8859-1"));
btGpsOutputStream.flush();
}
} catch (SecurityException se) {
/**
* The application was not permitted to connect to bluetooth
*/
receiverList.receiveMessage(Locale.get("btreceiverinput.AlBTConnectNotPermit")/*Connecting to BT not permitted*/);
return false;
} catch (IOException e) {
receiverList.receiveMessage(Locale.get("btreceiverinput.AlErr")/*BT error:*/ + e.getMessage());
return false;
}
return true;
}
private synchronized void closeBtConnection() {
disableRawLogging();
if (btGpsInputStream != null){
try {
btGpsInputStream.close();
} catch (IOException e) {
}
btGpsInputStream=null;
}
if (btGpsOutputStream != null){
try {
if (Configuration.getLocationProvider() == Configuration.LOCATIONPROVIDER_GPSD) {
btGpsOutputStream.write("?WATCH={\"enable\":false}".getBytes("ISO-8859-1"));
btGpsOutputStream.flush();
}
btGpsOutputStream.close();
} catch (IOException e) {
}
btGpsOutputStream=null;
}
if (conn != null){
try {
conn.close();
} catch (IOException e) {
}
conn=null;
}
}
/**
* This function tries to reconnect to the bluetooth
* it retries for up to 40 seconds and blocks in the
* mean time, so this function has to be called from
* within a separate thread. If successful, it will
* reinitialise the location producer with the new
* streams.
*
* @return whether the reconnect was successful
*/
private boolean autoReconnectBtConnection() {
if (!Configuration.getBtAutoRecon()) {
logger.info("Not trying to reconnect");
return false;
}
if (Configuration.getCfgBitState(Configuration.CFGBIT_SND_DISCONNECT)) {
ShareNav.mNoiseMaker.playSound("DISCONNECT");
}
/**
* If there are still parts of the old connection
* left over, close these cleanly.
*/
closeBtConnection();
int reconnectFailures = 0;
logger.info("Trying to reconnect to bluetooth");
while (!closed && (reconnectFailures < 4) && (! openBtConnection(Configuration.getBtUrl()))){
reconnectFailures++;
logger.info("Failed to reconnect for the " + reconnectFailures + " time");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
logger.silentexception("INTERRUPTED!", e);
return false;
}
}
if (reconnectFailures < 8 && !closed) {
if (Configuration.getCfgBitState(Configuration.CFGBIT_SND_CONNECT)) {
ShareNav.mNoiseMaker.playSound("CONNECT");
}
init(null);
return true;
}
if (!closed) {
logger.error(Locale.get("btreceiverinput.ErLostConnection")/*Lost connection to GPS and failed to reconnect*/);
}
return false;
}
}