altosdroid: Add USB support for TeleDongle/TeleBT
authorKeith Packard <keithp@keithp.com>
Tue, 28 Apr 2015 04:20:22 +0000 (21:20 -0700)
committerKeith Packard <keithp@keithp.com>
Tue, 28 Apr 2015 04:20:22 +0000 (21:20 -0700)
This lets AltosDroid use a USB-connected receiver as well as Bluetooth devices.

Signed-off-by: Keith Packard <keithp@keithp.com>
altosdroid/AndroidManifest.xml
altosdroid/default.properties
altosdroid/project.properties
altosdroid/res/values/CustomTheme.xml [new file with mode: 0644]
altosdroid/res/xml/device_filter.xml [new file with mode: 0644]
altosdroid/src/org/altusmetrum/AltosDroid/AltosBluetooth.java
altosdroid/src/org/altusmetrum/AltosDroid/AltosDroid.java
altosdroid/src/org/altusmetrum/AltosDroid/AltosDroidLink.java [new file with mode: 0644]
altosdroid/src/org/altusmetrum/AltosDroid/AltosUsb.java [new file with mode: 0644]
altosdroid/src/org/altusmetrum/AltosDroid/TelemetryReader.java
altosdroid/src/org/altusmetrum/AltosDroid/TelemetryService.java

index 19e5a6dc5e0e4f390a8ee8d4b98f2b1a9fdbeece..71c6fb1256aad59a5fc87c4e515591b534e170c6 100644 (file)
@@ -19,7 +19,7 @@
           package="org.altusmetrum.AltosDroid"
           android:versionCode="6"
           android:versionName="1.5">
-    <uses-sdk android:targetSdkVersion="10" android:minSdkVersion="10"/>
+    <uses-sdk android:targetSdkVersion="12" android:minSdkVersion="12"/>
     <!-- Google Maps -->
     <uses-feature android:glEsVersion="0x00020000" android:required="true"/>
 
                 android:protectionLevel="signature"/>
     <uses-permission android:name="org.altusmetrum.AltosDroid.permission.MAPS_RECEIVE"/>
 
+    <!-- Permissions needed to access USB OTG -->
+    <uses-feature android:name="android.hardware.usb.host" />
 
     <application android:label="@string/app_name"
                  android:icon="@drawable/app_icon"
-                 android:allowBackup="true" >
+                 android:allowBackup="true"
+                android:theme="@style/CustomTheme">
         <activity android:name="org.altusmetrum.AltosDroid.AltosDroid"
                   android:label="@string/app_name"
-                  android:configChanges="orientation|keyboardHidden" >
+                  android:configChanges="orientation|keyboardHidden"
+                 android:launchMode="singleTop">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
+       <activity android:name="org.altusmetrum.AltosDroid.AltosDroid"
+                  android:configChanges="orientation|keyboardHidden"
+                 android:launchMode="singleTop">
+         <intent-filter>
+           <action
+               android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
+         </intent-filter>
+         <meta-data
+             android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
+             android:resource="@xml/device_filter" />
+       </activity>
+
         <activity android:name=".DeviceListActivity"
                   android:label="@string/select_device"
                   android:theme="@android:style/Theme.Dialog"
index 66db0d159b1fe234f3fe34fc2a0743ca579d84c9..3ac252342cbe9ef944aee82ab310cf77d8366cdd 100644 (file)
@@ -8,4 +8,4 @@
 # project structure.
 
 # Project target.
-target=android-10
+target=android-12
index 96b9551ce02b63037f437646af32581a9c4e5a5a..d178f98a978e2159ce92c182fe3edf30e9ace47f 100644 (file)
@@ -8,5 +8,5 @@
 # project structure.
 
 # Project target.
-target=android-10
+target=android-12
 android.library.reference.1=google-play-services_lib/
diff --git a/altosdroid/res/values/CustomTheme.xml b/altosdroid/res/values/CustomTheme.xml
new file mode 100644 (file)
index 0000000..4daed1f
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<resources>
+  <style name="CustomTheme" parent="android:Theme">
+    <item name="android:windowNoTitle">false</item>
+  </style>
+</resources>
diff --git a/altosdroid/res/xml/device_filter.xml b/altosdroid/res/xml/device_filter.xml
new file mode 100644 (file)
index 0000000..84b09c0
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<resources>
+  <usb-device vendor-id="65534" />
+  <usb-device vendor-id="1027" />
+</resources>
index da75ffddaf5ffdbd0a8c82049a16cba7f650e751..32b7a65bc3e45578d2459aa41b391a38cabd515e 100644 (file)
@@ -33,16 +33,13 @@ import android.util.Log;
 
 import org.altusmetrum.altoslib_6.*;
 
-public class AltosBluetooth extends AltosLink {
+public class AltosBluetooth extends AltosDroidLink {
 
        // Debugging
        private static final String TAG = "AltosBluetooth";
        private static final boolean D = true;
 
        private ConnectThread    connect_thread = null;
-       private Thread           input_thread   = null;
-
-       private Handler          handler;
 
        private BluetoothAdapter adapter;
        private BluetoothSocket  socket;
@@ -51,6 +48,7 @@ public class AltosBluetooth extends AltosLink {
 
        // Constructor
        public AltosBluetooth(BluetoothDevice device, Handler handler) {
+               super(handler);
 //             set_debug(D);
                adapter = BluetoothAdapter.getDefaultAdapter();
                this.handler = handler;
@@ -60,16 +58,7 @@ public class AltosBluetooth extends AltosLink {
                connect_thread.start();
        }
 
-       private Object closed_lock = new Object();
-       private boolean closed = false;
-
-       private boolean closed() {
-               synchronized(closed_lock) {
-                       return closed;
-               }
-       }
-
-       private void connected() {
+       void connected() {
                if (closed()) {
                        if (D) Log.d(TAG, "connected after closed");
                        return;
@@ -80,24 +69,11 @@ public class AltosBluetooth extends AltosLink {
                                if (socket != null) {
                                        input = socket.getInputStream();
                                        output = socket.getOutputStream();
-
-                                       input_thread = new Thread(this);
-                                       input_thread.start();
-
-                                       // Configure the newly connected device for telemetry
-                                       print("~\nE 0\n");
-                                       set_monitor(false);
-                                       if (D) Log.d(TAG, "ConnectThread: connected");
-
-                                       /* Let TelemetryService know we're connected
-                                        */
-                                       handler.obtainMessage(TelemetryService.MSG_CONNECTED, this).sendToTarget();
-
-                                       /* Notify other waiting threads that we're connected now
-                                        */
-                                       notifyAll();
+                                       super.connected();
                                }
                        }
+               } catch (InterruptedException ie) {
+                       connect_failed();
                } catch (IOException io) {
                        connect_failed();
                }
@@ -109,24 +85,14 @@ public class AltosBluetooth extends AltosLink {
                        return;
                }
 
-               close_socket();
+               close_device();
                input = null;
                output = null;
                handler.obtainMessage(TelemetryService.MSG_CONNECT_FAILED, this).sendToTarget();
                if (D) Log.e(TAG, "ConnectThread: Failed to establish connection");
        }
 
-       private void disconnected() {
-               if (closed()) {
-                       if (D) Log.d(TAG, "disconnected after closed");
-                       return;
-               }
-
-               if (D) Log.d(TAG, "Sending disconnected message");
-               handler.obtainMessage(TelemetryService.MSG_DISCONNECTED, this).sendToTarget();
-       }
-
-       private void close_socket() {
+       void close_device() {
                BluetoothSocket tmp_socket;
 
                synchronized(this) {
@@ -143,6 +109,12 @@ public class AltosBluetooth extends AltosLink {
                }
        }
 
+       public void close() {
+               super.close();
+               input = null;
+               output = null;
+       }
+
        private final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
 
        private void create_socket(BluetoothDevice  device) {
@@ -156,7 +128,7 @@ public class AltosBluetooth extends AltosLink {
                }
                if (socket != null) {
                        if (D) Log.d(TAG, String.format("Socket already allocated %s", socket.toString()));
-                       close_socket();
+                       close_device();
                }
                synchronized (this) {
                        socket = tmp_socket;
@@ -205,22 +177,6 @@ public class AltosBluetooth extends AltosLink {
                }
        }
 
-       public double frequency() {
-               return frequency;
-       }
-
-       public int telemetry_rate() {
-               return telemetry_rate;
-       }
-
-       public void save_frequency() {
-               AltosPreferences.set_frequency(0, frequency);
-       }
-
-       public void save_telemetry_rate() {
-               AltosPreferences.set_telemetry_rate(0, telemetry_rate);
-       }
-
        private synchronized void wait_connected() throws InterruptedException, IOException {
                if (input == null && socket != null) {
                        if (D) Log.d(TAG, "wait_connected...");
@@ -231,109 +187,23 @@ public class AltosBluetooth extends AltosLink {
                        throw new IOException();
        }
 
-       public void print(String data) {
-               byte[] bytes = data.getBytes();
-               if (D) Log.d(TAG, "print(): begin");
+       int write(byte[] buffer, int len) {
                try {
-                       wait_connected();
-                       output.write(bytes);
-                       if (D) Log.d(TAG, "print(): Wrote bytes: '" + data.replace('\n', '\\') + "'");
-               } catch (IOException e) {
-                       disconnected();
-               } catch (InterruptedException e) {
-                       disconnected();
+                       output.write(buffer, 0, len);
+               } catch (IOException ie) {
+                       return -1;
                }
+               return len;
        }
 
-       public void putchar(byte c) {
-               byte[] bytes = { c };
-               if (D) Log.d(TAG, "print(): begin");
+       int read(byte[] buffer, int len) {
                try {
-                       wait_connected();
-                       output.write(bytes);
-                       if (D) Log.d(TAG, "print(): Wrote byte: '" + c + "'");
-               } catch (IOException e) {
-                       disconnected();
-               } catch (InterruptedException e) {
-                       disconnected();
-               }
-       }
-
-       private static final int buffer_size = 1024;
-
-       private byte[] buffer = new byte[buffer_size];
-       private int buffer_len = 0;
-       private int buffer_off = 0;
-
-       private byte[] debug_chars = new byte[buffer_size];
-       private int debug_off;
-
-       private void debug_input(byte b) {
-               if (b == '\n') {
-                       Log.d(TAG, "            " + new String(debug_chars, 0, debug_off));
-                       debug_off = 0;
-               } else {
-                       if (debug_off < buffer_size)
-                               debug_chars[debug_off++] = b;
+                       return input.read(buffer, 0, len);
+               } catch (IOException ie) {
+                       return -1;
                }
        }
 
-       public int getchar() {
-               while (buffer_off == buffer_len) {
-                       try {
-                               wait_connected();
-                               buffer_len = input.read(buffer);
-                               buffer_off = 0;
-                       } catch (IOException e) {
-                               if (D) Log.d(TAG, "getchar IOException");
-                               disconnected();
-                               return AltosLink.ERROR;
-                       } catch (java.lang.InterruptedException e) {
-                               if (D) Log.d(TAG, "getchar Interrupted");
-                               disconnected();
-                               return AltosLink.ERROR;
-                       }
-               }
-               if (D)
-                       debug_input(buffer[buffer_off]);
-               return buffer[buffer_off++];
-       }
-
-       public void closing() {
-               synchronized(closed_lock) {
-                       if (D) Log.d(TAG, "Marked closed true");
-                       closed = true;
-               }
-       }
-
-
-       public void close() {
-               if (D) Log.d(TAG, "close(): begin");
-
-               closing();
-
-               close_socket();
-
-               synchronized(this) {
-
-                       if (input_thread != null) {
-                               if (D) Log.d(TAG, "close(): stopping input_thread");
-                               try {
-                                       if (D) Log.d(TAG, "close(): input_thread.interrupt().....");
-                                       input_thread.interrupt();
-                                       if (D) Log.d(TAG, "close(): input_thread.join().....");
-                                       input_thread.join();
-                               } catch (Exception e) {}
-                               input_thread = null;
-                       }
-                       input = null;
-                       output = null;
-                       notifyAll();
-               }
-       }
-
-       //public void flush_output() { super.flush_output(); }
-
        // Stubs of required methods when extending AltosLink
        public boolean can_cancel_reply()   { return false; }
        public boolean show_reply_timeout() { return true; }
index 4e7bdd6b331dc7b2d3bd10274d6f75e00f81aef0..27ebf20685cfbd3d788e873412a42968857b41d0 100644 (file)
@@ -23,6 +23,7 @@ import java.util.Timer;
 import java.util.TimerTask;
 
 import android.app.Activity;
+import android.app.PendingIntent;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.content.Intent;
@@ -51,6 +52,7 @@ import android.widget.RelativeLayout;
 import android.widget.Toast;
 import android.app.AlertDialog;
 import android.location.Location;
+import android.hardware.usb.*;
 
 import org.altusmetrum.altoslib_6.*;
 
@@ -59,6 +61,11 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
        static final String TAG = "AltosDroid";
        static final boolean D = true;
 
+       // Actions sent to the telemetry server at startup time
+
+       public static final String ACTION_BLUETOOTH = "org.altusmetrum.AltosDroid.BLUETOOTH";
+       public static final String ACTION_USB = "org.altusmetrum.AltosDroid.USB";
+
        // Message types received by our Handler
 
        public static final int MSG_STATE           = 1;
@@ -101,6 +108,9 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
        private Timer timer;
        AltosState saved_state;
 
+       UsbDevice       pending_usb_device;
+       boolean         start_with_usb;
+
        // Service
        private boolean mIsBound   = false;
        private Messenger mService = null;
@@ -148,6 +158,13 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
                        } catch (RemoteException e) {
                                // In this case the service has crashed before we could even do anything with it
                        }
+                       if (pending_usb_device != null) {
+                               try {
+                                       mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, pending_usb_device));
+                                       pending_usb_device = null;
+                               } catch (RemoteException e) {
+                               }
+                       }
                }
 
                public void onServiceDisconnected(ComponentName className) {
@@ -395,15 +412,6 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
                super.onCreate(savedInstanceState);
                if(D) Log.e(TAG, "+++ ON CREATE +++");
 
-               // Get local Bluetooth adapter
-               mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-
-               // If the adapter is null, then Bluetooth is not supported
-               if (mBluetoothAdapter == null) {
-                       Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
-                       finish();
-               }
-
                fm = getSupportFragmentManager();
 
                // Set up the window layout
@@ -467,23 +475,117 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
                mAgeView       = (TextView) findViewById(R.id.age_value);
        }
 
+       private boolean ensureBluetooth() {
+               // Get local Bluetooth adapter
+               mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+               // If the adapter is null, then Bluetooth is not supported
+               if (mBluetoothAdapter == null) {
+                       Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
+                       return false;
+               }
+
+               if (!mBluetoothAdapter.isEnabled()) {
+                       Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+                       startActivityForResult(enableIntent, AltosDroid.REQUEST_ENABLE_BT);
+               }
+
+               return true;
+       }
+
+       private boolean check_usb() {
+               UsbDevice       device = AltosUsb.find_device(this, AltosLib.product_basestation);
+
+               if (device != null) {
+                       Intent          i = new Intent(this, AltosDroid.class);
+                       PendingIntent   pi = PendingIntent.getActivity(this, 0, new Intent("hello world", null, this, AltosDroid.class), 0);
+
+                       if (AltosUsb.request_permission(this, device, pi)) {
+                               connectUsb(device);
+                       }
+                       start_with_usb = true;
+                       return true;
+               }
+
+               start_with_usb = false;
+
+               return false;
+       }
+
+       private void noticeIntent(Intent intent) {
+
+               /* Ok, this is pretty convenient.
+                *
+                * When a USB device is plugged in, and our 'hotplug'
+                * intent registration fires, we get an Intent with
+                * EXTRA_DEVICE set.
+                *
+                * When we start up and see a usb device and request
+                * permission to access it, that queues a
+                * PendingIntent, which has the EXTRA_DEVICE added in,
+                * along with the EXTRA_PERMISSION_GRANTED field as
+                * well.
+                *
+                * So, in both cases, we get the device name using the
+                * same call. We check to see if access was granted,
+                * in which case we ignore the device field and do our
+                * usual startup thing.
+                */
+
+               UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+               boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true);
+
+               if (D) Log.e(TAG, "intent " + intent + " device " + device + " granted " + granted);
+
+               if (!granted)
+                       device = null;
+
+               if (device != null) {
+                       if (D) Log.d(TAG, "intent has usb device " + device.toString());
+                       connectUsb(device);
+               } else {
+
+                       /* 'granted' is only false if this intent came
+                        * from the request_permission call and
+                        * permission was denied. In which case, we
+                        * don't want to loop forever...
+                        */
+                       if (granted) {
+                               if (D) Log.d(TAG, "check for a USB device at startup");
+                               if (check_usb())
+                                       return;
+                       }
+                       if (D) Log.d(TAG, "Starting by looking for bluetooth devices");
+                       if (ensureBluetooth())
+                               return;
+                       finish();
+               }
+       }
+
        @Override
        public void onStart() {
                super.onStart();
                if(D) Log.e(TAG, "++ ON START ++");
 
+               noticeIntent(getIntent());
+
                // Start Telemetry Service
-               startService(new Intent(AltosDroid.this, TelemetryService.class));
+               String  action = start_with_usb ? ACTION_USB : ACTION_BLUETOOTH;
 
-               if (!mBluetoothAdapter.isEnabled()) {
-                       Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
-                       startActivityForResult(enableIntent, AltosDroid.REQUEST_ENABLE_BT);
-               }
+               startService(new Intent(action, null, AltosDroid.this, TelemetryService.class));
 
                doBindService();
 
                if (mAltosVoice == null)
                        mAltosVoice = new AltosVoice(this);
+
+       }
+
+       @Override
+       public void onNewIntent(Intent intent) {
+               super.onNewIntent(intent);
+               if(D) Log.d(TAG, "onNewIntent");
+               noticeIntent(intent);
        }
 
        @Override
@@ -519,7 +621,7 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
                stop_timer();
        }
 
-       public void onActivityResult(int requestCode, int resultCode, Intent data) {
+       protected void onActivityResult(int requestCode, int resultCode, Intent data) {
                if(D) Log.d(TAG, "onActivityResult " + resultCode);
                switch (requestCode) {
                case REQUEST_CONNECT_DEVICE:
@@ -544,6 +646,20 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
                }
        }
 
+       private void connectUsb(UsbDevice device) {
+               if (mService == null)
+                       pending_usb_device = device;
+               else {
+                       // Attempt to connect to the device
+                       try {
+                               mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, device));
+                               if (D) Log.d(TAG, "Sent OPEN_USB message");
+                       } catch (RemoteException e) {
+                               if (D) Log.e(TAG, "connect device message failed");
+                       }
+               }
+       }
+
        private void connectDevice(Intent data) {
                // Attempt to connect to the device
                try {
@@ -619,12 +735,14 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
                Intent serverIntent = null;
                switch (item.getItemId()) {
                case R.id.connect_scan:
-                       // Launch the DeviceListActivity to see devices and do scan
-                       serverIntent = new Intent(this, DeviceListActivity.class);
-                       startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
+                       if (ensureBluetooth()) {
+                               // Launch the DeviceListActivity to see devices and do scan
+                               serverIntent = new Intent(this, DeviceListActivity.class);
+                               startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
+                       }
                        return true;
                case R.id.disconnect:
-                       /* Disconnect the bluetooth device
+                       /* Disconnect the device
                         */
                        disconnectDevice();
                        return true;
diff --git a/altosdroid/src/org/altusmetrum/AltosDroid/AltosDroidLink.java b/altosdroid/src/org/altusmetrum/AltosDroid/AltosDroidLink.java
new file mode 100644 (file)
index 0000000..badb2ca
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * Copyright © 2015 Keith Packard <keithp@keithp.com>
+ *
+ * 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; version 2 of the License.
+ *
+ * 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.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package org.altusmetrum.AltosDroid;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.UUID;
+
+import android.os.Handler;
+import android.util.Log;
+
+import org.altusmetrum.altoslib_6.*;
+
+public abstract class AltosDroidLink extends AltosLink {
+
+       // Debugging
+       private static final String TAG = "AltosDroidLink";
+       private static final boolean D = true;
+
+       Handler         handler;
+
+       Thread          input_thread   = null;
+
+       public double frequency() {
+               return frequency;
+       }
+
+       public int telemetry_rate() {
+               return telemetry_rate;
+       }
+
+       public void save_frequency() {
+               AltosPreferences.set_frequency(0, frequency);
+       }
+
+       public void save_telemetry_rate() {
+               AltosPreferences.set_telemetry_rate(0, telemetry_rate);
+       }
+
+       Object closed_lock = new Object();
+       boolean closing = false;
+       boolean closed = false;
+
+       public boolean closed() {
+               synchronized(closed_lock) {
+                       return closing;
+               }
+       }
+
+       void connected() throws InterruptedException {
+               input_thread = new Thread(this);
+               input_thread.start();
+
+               // Configure the newly connected device for telemetry
+               print("~\nE 0\n");
+               set_monitor(false);
+               if (D) Log.d(TAG, "ConnectThread: connected");
+
+               /* Let TelemetryService know we're connected
+                */
+               handler.obtainMessage(TelemetryService.MSG_CONNECTED, this).sendToTarget();
+
+               /* Notify other waiting threads that we're connected now
+                */
+               notifyAll();
+       }
+
+       public void closing() {
+               synchronized(closed_lock) {
+                       if (D) Log.d(TAG, "Marked closing true");
+                       closing = true;
+               }
+       }
+
+       private boolean actually_closed() {
+               synchronized(closed_lock) {
+                       return closed;
+               }
+       }
+
+       abstract void close_device();
+
+       public void close() {
+               if (D) Log.d(TAG, "close(): begin");
+
+               closing();
+
+               flush_output();
+
+               synchronized (closed_lock) {
+                       if (D) Log.d(TAG, "Marked closed true");
+                       closed = true;
+               }
+
+               close_device();
+
+               synchronized(this) {
+
+                       if (input_thread != null) {
+                               if (D) Log.d(TAG, "close(): stopping input_thread");
+                               try {
+                                       if (D) Log.d(TAG, "close(): input_thread.interrupt().....");
+                                       input_thread.interrupt();
+                                       if (D) Log.d(TAG, "close(): input_thread.join().....");
+                                       input_thread.join();
+                               } catch (Exception e) {}
+                               input_thread = null;
+                       }
+                       notifyAll();
+               }
+       }
+
+       abstract int write(byte[] buffer, int len);
+
+       abstract int read(byte[] buffer, int len);
+
+       private static final int buffer_size = 64;
+
+       private byte[] in_buffer = new byte[buffer_size];
+       private byte[] out_buffer = new byte[buffer_size];
+       private int buffer_len = 0;
+       private int buffer_off = 0;
+       private int out_buffer_off = 0;
+
+       private byte[] debug_chars = new byte[buffer_size];
+       private int debug_off;
+
+       private void debug_input(byte b) {
+               if (b == '\n') {
+                       Log.d(TAG, "            " + new String(debug_chars, 0, debug_off));
+                       debug_off = 0;
+               } else {
+                       if (debug_off < buffer_size)
+                               debug_chars[debug_off++] = b;
+               }
+       }
+
+       private void disconnected() {
+               if (closed()) {
+                       if (D) Log.d(TAG, "disconnected after closed");
+                       return;
+               }
+
+               if (D) Log.d(TAG, "Sending disconnected message");
+               handler.obtainMessage(TelemetryService.MSG_DISCONNECTED, this).sendToTarget();
+       }
+
+       public int getchar() {
+
+               if (actually_closed())
+                       return ERROR;
+
+               while (buffer_off == buffer_len) {
+                       buffer_len = read(in_buffer, buffer_size);
+                       if (buffer_len < 0) {
+                               Log.d(TAG, "ERROR returned from getchar()");
+                               disconnected();
+                               return ERROR;
+                       }
+                       buffer_off = 0;
+               }
+               if (D)
+                       debug_input(in_buffer[buffer_off]);
+               return in_buffer[buffer_off++];
+       }
+
+       public void flush_output() {
+               super.flush_output();
+
+               if (actually_closed()) {
+                       out_buffer_off = 0;
+                       return;
+               }
+
+               while (out_buffer_off != 0) {
+                       int     sent = write(out_buffer, out_buffer_off);
+
+                       if (sent <= 0) {
+                               Log.d(TAG, "flush_output() failed");
+                               out_buffer_off = 0;
+                               break;
+                       }
+
+                       if (sent < out_buffer_off)
+                               System.arraycopy(out_buffer, 0, out_buffer, sent, out_buffer_off - sent);
+
+                       out_buffer_off -= sent;
+               }
+       }
+
+       public void putchar(byte c) {
+               out_buffer[out_buffer_off++] = c;
+               if (out_buffer_off == buffer_size)
+                       flush_output();
+       }
+
+       public void print(String data) {
+               byte[] bytes = data.getBytes();
+               if (D) Log.d(TAG, "print(): begin");
+               for (byte b : bytes)
+                       putchar(b);
+               if (D) Log.d(TAG, "print(): Wrote bytes: '" + data.replace('\n', '\\') + "'");
+       }
+
+       public AltosDroidLink(Handler handler) {
+               this.handler = handler;
+       }
+}
diff --git a/altosdroid/src/org/altusmetrum/AltosDroid/AltosUsb.java b/altosdroid/src/org/altusmetrum/AltosDroid/AltosUsb.java
new file mode 100644 (file)
index 0000000..81d50ab
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * Copyright © 2015 Keith Packard <keithp@keithp.com>
+ *
+ * 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; version 2 of the License.
+ *
+ * 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.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package org.altusmetrum.AltosDroid;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.UUID;
+import java.util.HashMap;
+
+import android.content.Context;
+import android.hardware.usb.*;
+import android.app.*;
+import android.os.Handler;
+import android.util.Log;
+
+import org.altusmetrum.altoslib_6.*;
+
+public class AltosUsb extends AltosDroidLink {
+
+       // Debugging
+       private static final String TAG = "AltosUsb";
+       private static final boolean D = true;
+
+       private Thread           input_thread   = null;
+
+       private Handler          handler;
+
+       private UsbManager              manager;
+       private UsbDevice               device;
+       private UsbDeviceConnection     connection;
+       private UsbInterface            iface;
+       private UsbEndpoint             in, out;
+
+       private InputStream      input;
+       private OutputStream     output;
+
+       // Constructor
+       public AltosUsb(Context context, UsbDevice device, Handler handler) {
+               super(handler);
+//             set_debug(D);
+               this.handler = handler;
+
+               iface = null;
+               in = null;
+               out = null;
+
+               int     niface = device.getInterfaceCount();
+
+               for (int i = 0; i < niface; i++) {
+
+                       iface = device.getInterface(i);
+
+                       in = null;
+                       out = null;
+
+                       int nendpoints = iface.getEndpointCount();
+
+                       for (int e = 0; e < nendpoints; e++) {
+                               UsbEndpoint     endpoint = iface.getEndpoint(e);
+
+                               if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
+                                       switch (endpoint.getDirection()) {
+                                       case UsbConstants.USB_DIR_OUT:
+                                               out = endpoint;
+                                               break;
+                                       case UsbConstants.USB_DIR_IN:
+                                               in = endpoint;
+                                               break;
+                                       }
+                               }
+                       }
+
+                       if (in != null && out != null)
+                               break;
+               }
+
+               if (in != null && out != null) {
+                       Log.d(TAG, String.format("\tin %s out %s\n", in.toString(), out.toString()));
+
+                       manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
+
+                       if (manager == null) {
+                               Log.d(TAG, "USB_SERVICE failed");
+                               return;
+                       }
+
+                       connection = manager.openDevice(device);
+
+                       if (connection == null) {
+                               Log.d(TAG, "openDevice failed");
+                               return;
+                       }
+
+                       connection.claimInterface(iface, true);
+
+                       input_thread = new Thread(this);
+                       input_thread.start();
+
+                       // Configure the newly connected device for telemetry
+                       print("~\nE 0\n");
+                       set_monitor(false);
+               }
+       }
+
+       static private boolean isAltusMetrum(UsbDevice device) {
+               if (device.getVendorId() != AltosLib.vendor_altusmetrum)
+                       return false;
+               if (device.getProductId() < AltosLib.product_altusmetrum_min)
+                       return false;
+               if (device.getProductId() > AltosLib.product_altusmetrum_max)
+                       return false;
+               return true;
+       }
+
+       static boolean matchProduct(int want_product, UsbDevice device) {
+
+               if (!isAltusMetrum(device))
+                       return false;
+
+               if (want_product == AltosLib.product_any)
+                       return true;
+
+               int have_product = device.getProductId();
+
+               if (want_product == AltosLib.product_basestation)
+                       return have_product == AltosLib.product_teledongle ||
+                               have_product == AltosLib.product_teleterra ||
+                               have_product == AltosLib.product_telebt ||
+                               have_product == AltosLib.product_megadongle;
+
+               if (want_product == AltosLib.product_altimeter)
+                       return have_product == AltosLib.product_telemetrum ||
+                               have_product == AltosLib.product_telemega ||
+                               have_product == AltosLib.product_easymega ||
+                               have_product == AltosLib.product_telegps ||
+                               have_product == AltosLib.product_easymini ||
+                               have_product == AltosLib.product_telemini;
+
+               if (have_product == AltosLib.product_altusmetrum)       /* old devices match any request */
+                       return true;
+
+               if (want_product == have_product)
+                       return true;
+
+               return false;
+       }
+
+       static public boolean request_permission(Context context, UsbDevice device, PendingIntent pi) {
+               UsbManager      manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
+
+//             if (manager.hasPermission(device))
+//                     return true;
+
+               Log.d(TAG, "request permission for USB device " + device.toString());
+
+               manager.requestPermission(device, pi);
+               return false;
+       }
+
+       static public UsbDevice find_device(Context context, int match_product) {
+               UsbManager      manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
+
+               HashMap<String,UsbDevice>       devices = manager.getDeviceList();
+
+               for (UsbDevice  device : devices.values()) {
+                       int     vendor = device.getVendorId();
+                       int     product = device.getProductId();
+
+                       if (matchProduct(match_product, device)) {
+                               Log.d(TAG, "found USB device " + device.toString());
+                               return device;
+                       }
+               }
+
+               return null;
+       }
+
+       private void disconnected() {
+               if (closed()) {
+                       if (D) Log.d(TAG, "disconnected after closed");
+                       return;
+               }
+
+               if (D) Log.d(TAG, "Sending disconnected message");
+               handler.obtainMessage(TelemetryService.MSG_DISCONNECTED, this).sendToTarget();
+       }
+
+       void close_device() {
+               UsbDeviceConnection     tmp_connection;
+
+               synchronized(this) {
+                       tmp_connection = connection;
+                       connection = null;
+               }
+
+               if (tmp_connection != null) {
+                       if (D) Log.d(TAG, "Closing USB device");
+                       tmp_connection.close();
+               }
+       }
+
+       int read(byte[] buffer, int len) {
+               int ret = connection.bulkTransfer(in, buffer, len, -1);
+               if (D) Log.d(TAG, String.format("read(%d) = %d\n", len, ret));
+               return ret;
+       }
+
+       int write(byte[] buffer, int len) {
+               int ret = connection.bulkTransfer(out, buffer, len, -1);
+               if (D) Log.d(TAG, String.format("write(%d) = %d\n", len, ret));
+               return ret;
+       }
+
+       // Stubs of required methods when extending AltosLink
+       public boolean can_cancel_reply()   { return false; }
+       public boolean show_reply_timeout() { return true; }
+       public void hide_reply_timeout()    { }
+
+}
index 4e4408d5edc0c44b5865f2a0520c8d84e23f246c..bdb2bae441e078c8bd03ebe57a307a3b7f4d850b 100644 (file)
@@ -81,6 +81,8 @@ public class TelemetryReader extends Thread {
                        }
                } catch (InterruptedException ee) {
                } catch (IOException ie) {
+                       Log.e(TAG, "IO exception in telemetry reader");
+                       handler.obtainMessage(TelemetryService.MSG_DISCONNECTED, link).sendToTarget();
                } finally {
                        close();
                }
index 65eabf1166a394f5fdeb4a584dddbd765594b385..e7f958b9e4c08f173a3116b59019ec1720099203 100644 (file)
@@ -29,6 +29,7 @@ import android.app.PendingIntent;
 import android.app.Service;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothAdapter;
+import android.hardware.usb.*;
 import android.content.Intent;
 import android.content.Context;
 import android.os.Bundle;
@@ -56,14 +57,15 @@ public class TelemetryService extends Service implements LocationListener {
        static final int MSG_REGISTER_CLIENT   = 1;
        static final int MSG_UNREGISTER_CLIENT = 2;
        static final int MSG_CONNECT           = 3;
-       static final int MSG_CONNECTED         = 4;
-       static final int MSG_CONNECT_FAILED    = 5;
-       static final int MSG_DISCONNECTED      = 6;
-       static final int MSG_TELEMETRY         = 7;
-       static final int MSG_SETFREQUENCY      = 8;
-       static final int MSG_CRC_ERROR         = 9;
-       static final int MSG_SETBAUD           = 10;
-       static final int MSG_DISCONNECT        = 11;
+       static final int MSG_OPEN_USB          = 4;
+       static final int MSG_CONNECTED         = 5;
+       static final int MSG_CONNECT_FAILED    = 6;
+       static final int MSG_DISCONNECTED      = 7;
+       static final int MSG_TELEMETRY         = 8;
+       static final int MSG_SETFREQUENCY      = 9;
+       static final int MSG_CRC_ERROR         = 10;
+       static final int MSG_SETBAUD           = 11;
+       static final int MSG_DISCONNECT        = 12;
 
        // Unique Identification Number for the Notification.
        // We use it on Notification start, and to cancel it.
@@ -76,7 +78,7 @@ public class TelemetryService extends Service implements LocationListener {
 
        // Name of the connected device
        DeviceAddress address;
-       private AltosBluetooth  altos_bluetooth  = null;
+       private AltosDroidLink  altos_link  = null;
        private TelemetryReader telemetry_reader = null;
        private TelemetryLogger telemetry_logger = null;
 
@@ -94,7 +96,7 @@ public class TelemetryService extends Service implements LocationListener {
                @Override
                public void handleMessage(Message msg) {
                        TelemetryService s = service.get();
-                       AltosBluetooth bt = null;
+                       AltosDroidLink bt = null;
                        if (s == null)
                                return;
                        switch (msg.what) {
@@ -112,18 +114,23 @@ public class TelemetryService extends Service implements LocationListener {
                                AltosDroidPreferences.set_active_device(address);
                                s.start_altos_bluetooth(address, false);
                                break;
+                       case MSG_OPEN_USB:
+                               if (D) Log.d(TAG, "Open USB command received");
+                               UsbDevice device = (UsbDevice) msg.obj;
+                               s.start_usb(device);
+                               break;
                        case MSG_DISCONNECT:
                                if (D) Log.d(TAG, "Disconnect command received");
                                s.address = null;
-                               s.stop_altos_bluetooth(true);
+                               s.disconnect(true);
                                break;
                        case MSG_SETFREQUENCY:
                                if (D) Log.d(TAG, "MSG_SETFREQUENCY");
                                s.telemetry_state.frequency = (Double) msg.obj;
                                if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) {
                                        try {
-                                               s.altos_bluetooth.set_radio_frequency(s.telemetry_state.frequency);
-                                               s.altos_bluetooth.save_frequency();
+                                               s.altos_link.set_radio_frequency(s.telemetry_state.frequency);
+                                               s.altos_link.save_frequency();
                                        } catch (InterruptedException e) {
                                        } catch (TimeoutException e) {
                                        }
@@ -134,8 +141,8 @@ public class TelemetryService extends Service implements LocationListener {
                                if (D) Log.d(TAG, "MSG_SETBAUD");
                                s.telemetry_state.telemetry_rate = (Integer) msg.obj;
                                if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) {
-                                       s.altos_bluetooth.set_telemetry_rate(s.telemetry_state.telemetry_rate);
-                                       s.altos_bluetooth.save_telemetry_rate();
+                                       s.altos_link.set_telemetry_rate(s.telemetry_state.telemetry_rate);
+                                       s.altos_link.save_telemetry_rate();
                                }
                                s.send_to_clients();
                                break;
@@ -145,9 +152,9 @@ public class TelemetryService extends Service implements LocationListener {
                                 */
                        case MSG_CONNECTED:
                                Log.d(TAG, "MSG_CONNECTED");
-                               bt = (AltosBluetooth) msg.obj;
+                               bt = (AltosDroidLink) msg.obj;
 
-                               if (bt != s.altos_bluetooth) {
+                               if (bt != s.altos_link) {
                                        if (D) Log.d(TAG, "Stale message");
                                        break;
                                }
@@ -159,9 +166,9 @@ public class TelemetryService extends Service implements LocationListener {
                                break;
                        case MSG_CONNECT_FAILED:
                                Log.d(TAG, "MSG_CONNECT_FAILED");
-                               bt = (AltosBluetooth) msg.obj;
+                               bt = (AltosDroidLink) msg.obj;
 
-                               if (bt != s.altos_bluetooth) {
+                               if (bt != s.altos_link) {
                                        if (D) Log.d(TAG, "Stale message");
                                        break;
                                }
@@ -169,14 +176,16 @@ public class TelemetryService extends Service implements LocationListener {
                                        if (D) Log.d(TAG, "Connection failed... retrying");
                                        s.start_altos_bluetooth(s.address, true);
                                } else {
-                                       s.stop_altos_bluetooth(true);
+                                       s.disconnect(true);
                                }
                                break;
                        case MSG_DISCONNECTED:
+
+                               /* This can be sent by either AltosDroidLink or TelemetryReader */
                                Log.d(TAG, "MSG_DISCONNECTED");
-                               bt = (AltosBluetooth) msg.obj;
+                               bt = (AltosDroidLink) msg.obj;
 
-                               if (bt != s.altos_bluetooth) {
+                               if (bt != s.altos_link) {
                                        if (D) Log.d(TAG, "Stale message");
                                        break;
                                }
@@ -184,7 +193,7 @@ public class TelemetryService extends Service implements LocationListener {
                                        if (D) Log.d(TAG, "Connection lost... retrying");
                                        s.start_altos_bluetooth(s.address, true);
                                } else {
-                                       s.stop_altos_bluetooth(true);
+                                       s.disconnect(true);
                                }
                                break;
 
@@ -275,16 +284,17 @@ public class TelemetryService extends Service implements LocationListener {
                        send_to_client(client, m);
        }
 
-       private void stop_altos_bluetooth(boolean notify) {
-               if (D) Log.d(TAG, "stop_altos_bluetooth(): begin");
+       private void disconnect(boolean notify) {
+               if (D) Log.d(TAG, "disconnect(): begin");
+
                telemetry_state.connect = TelemetryState.CONNECT_DISCONNECTED;
                telemetry_state.address = null;
 
-               if (altos_bluetooth != null)
-                       altos_bluetooth.closing();
+               if (altos_link != null)
+                       altos_link.closing();
 
                if (telemetry_reader != null) {
-                       if (D) Log.d(TAG, "stop_altos_bluetooth(): stopping TelemetryReader");
+                       if (D) Log.d(TAG, "disconnect(): stopping TelemetryReader");
                        telemetry_reader.interrupt();
                        try {
                                telemetry_reader.join();
@@ -293,31 +303,44 @@ public class TelemetryService extends Service implements LocationListener {
                        telemetry_reader = null;
                }
                if (telemetry_logger != null) {
-                       if (D) Log.d(TAG, "stop_altos_bluetooth(): stopping TelemetryLogger");
+                       if (D) Log.d(TAG, "disconnect(): stopping TelemetryLogger");
                        telemetry_logger.stop();
                        telemetry_logger = null;
                }
-               if (altos_bluetooth != null) {
-                       if (D) Log.d(TAG, "stop_altos_bluetooth(): stopping AltosBluetooth");
-                       altos_bluetooth.close();
-                       altos_bluetooth = null;
+               if (altos_link != null) {
+                       if (D) Log.d(TAG, "disconnect(): stopping AltosDroidLink");
+                       altos_link.close();
+                       altos_link = null;
                }
                telemetry_state.config = null;
                if (notify) {
-                       if (D) Log.d(TAG, "stop_altos_bluetooth(): send message to clients");
+                       if (D) Log.d(TAG, "disconnect(): send message to clients");
                        send_to_clients();
                        if (clients.isEmpty()) {
-                               if (D) Log.d(TAG, "stop_altos_bluetooth(): no clients, terminating");
+                               if (D) Log.d(TAG, "disconnect(): no clients, terminating");
                                stopSelf();
                        }
                }
        }
 
+       private void start_usb(UsbDevice device) {
+               AltosUsb        d = new AltosUsb(this, device, handler);
+
+               if (d != null) {
+                       disconnect(false);
+                       altos_link = d;
+                       try {
+                               connected();
+                       } catch (InterruptedException ie) {
+                       }
+               }
+       }
+
        private void start_altos_bluetooth(DeviceAddress address, boolean pause) {
                // Get the BLuetoothDevice object
                BluetoothDevice device = bluetooth_adapter.getRemoteDevice(address.address);
 
-               stop_altos_bluetooth(false);
+               disconnect(false);
                if (pause) {
                        try {
                                Thread.sleep(4000);
@@ -326,7 +349,7 @@ public class TelemetryService extends Service implements LocationListener {
                }
                this.address = address;
                if (D) Log.d(TAG, String.format("start_altos_bluetooth(): Connecting to %s (%s)", device.getName(), device.getAddress()));
-               altos_bluetooth = new AltosBluetooth(device, handler);
+               altos_link = new AltosBluetooth(device, handler);
                telemetry_state.connect = TelemetryState.CONNECT_CONNECTING;
                telemetry_state.address = address;
                send_to_clients();
@@ -335,11 +358,11 @@ public class TelemetryService extends Service implements LocationListener {
        private void connected() throws InterruptedException {
                if (D) Log.d(TAG, "connected top");
                try {
-                       if (altos_bluetooth == null)
+                       if (altos_link == null)
                                throw new InterruptedException("no bluetooth");
-                       telemetry_state.config = altos_bluetooth.config_data();
-                       altos_bluetooth.set_radio_frequency(telemetry_state.frequency);
-                       altos_bluetooth.set_telemetry_rate(telemetry_state.telemetry_rate);
+                       telemetry_state.config = altos_link.config_data();
+                       altos_link.set_radio_frequency(telemetry_state.frequency);
+                       altos_link.set_telemetry_rate(telemetry_state.telemetry_rate);
                } catch (TimeoutException e) {
                        // If this timed out, then we really want to retry it, but
                        // probably safer to just retry the connection from scratch.
@@ -349,7 +372,7 @@ public class TelemetryService extends Service implements LocationListener {
                                start_altos_bluetooth(address, true);
                        } else {
                                handler.obtainMessage(MSG_CONNECT_FAILED).sendToTarget();
-                               stop_altos_bluetooth(true);
+                               disconnect(true);
                        }
                        return;
                }
@@ -358,12 +381,12 @@ public class TelemetryService extends Service implements LocationListener {
                telemetry_state.connect = TelemetryState.CONNECT_CONNECTED;
                telemetry_state.address = address;
 
-               telemetry_reader = new TelemetryReader(altos_bluetooth, handler, telemetry_state.state);
+               telemetry_reader = new TelemetryReader(altos_link, handler, telemetry_state.state);
                telemetry_reader.start();
 
                if (D) Log.d(TAG, "connected TelemetryReader started");
 
-               telemetry_logger = new TelemetryLogger(this, altos_bluetooth);
+               telemetry_logger = new TelemetryLogger(this, altos_link);
 
                if (D) Log.d(TAG, "Notify UI of connection");
 
@@ -403,10 +426,6 @@ public class TelemetryService extends Service implements LocationListener {
                LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
 
                locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, this);
-
-               DeviceAddress address = AltosDroidPreferences.active_device();
-               if (address != null)
-                       start_altos_bluetooth(address, false);
        }
 
        @Override
@@ -431,6 +450,14 @@ public class TelemetryService extends Service implements LocationListener {
                // Move us into the foreground.
                startForeground(NOTIFICATION, notification);
 
+               String  action = intent.getAction();
+
+               if (action.equals(AltosDroid.ACTION_BLUETOOTH)) {
+                       DeviceAddress address = AltosDroidPreferences.active_device();
+                       if (address != null && !address.address.startsWith("USB"))
+                               start_altos_bluetooth(address, false);
+               }
+
                // We want this service to continue running until it is explicitly
                // stopped, so return sticky.
                return START_STICKY;
@@ -443,7 +470,7 @@ public class TelemetryService extends Service implements LocationListener {
                ((LocationManager) getSystemService(Context.LOCATION_SERVICE)).removeUpdates(this);
 
                // Stop the bluetooth Comms threads
-               stop_altos_bluetooth(true);
+               disconnect(true);
 
                // Demote us from the foreground, and cancel the persistent notification.
                stopForeground(true);