diff --git a/QGCCommon.pri b/QGCCommon.pri index 7d9871261010c786a1912019ec5828e3bd26d68a..9c725866f58567bab31f4db2ed34fb9d78b664c0 100644 --- a/QGCCommon.pri +++ b/QGCCommon.pri @@ -36,6 +36,7 @@ linux { DEFINES += __android__ DEFINES += __STDC_LIMIT_MACROS DEFINES += QGC_ENABLE_BLUETOOTH + DEFINES += QGC_GST_TAISYNC_ENABLED target.path = $$DESTDIR equals(ANDROID_TARGET_ARCH, x86) { CONFIG += Androidx86Build diff --git a/android.pri b/android.pri index 8179a76f155b1cb3b9b46c595f8e2001a4309fb6..f655709bc50e8b137bf808938fba18a5bc075f6c 100644 --- a/android.pri +++ b/android.pri @@ -15,7 +15,8 @@ OTHER_FILES += \ $$PWD/android/src/com/hoho/android/usbserial/driver/UsbSerialProber.java \ $$PWD/android/src/com/hoho/android/usbserial/driver/UsbSerialRuntimeException.java \ $$PWD/android/src/org/mavlink/qgroundcontrol/QGCActivity.java \ - $$PWD/android/src/org/mavlink/qgroundcontrol/UsbIoManager.java + $$PWD/android/src/org/mavlink/qgroundcontrol/UsbIoManager.java \ + $$PWD/android/src/org/mavlink/qgroundcontrol/TaiSync.java DISTFILES += \ $$PWD/android/gradle/wrapper/gradle-wrapper.jar \ diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 28751cdc99aca2a34347ebdcc355e0deae7dd780..49581f61050e4c8ec5fc6946ab805384f7cbcd9c 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -7,10 +7,12 @@ + + @@ -60,13 +62,14 @@ - - + + - + - - + + + diff --git a/android/res/xml/device_filter.xml b/android/res/xml/device_filter.xml index 782fae8dd7e17709718b824ace9fa92e9479b52b..3d5af12260b8f854145c880e852faf6780cbacee 100644 --- a/android/res/xml/device_filter.xml +++ b/android/res/xml/device_filter.xml @@ -2,5 +2,6 @@ + diff --git a/android/src/org/mavlink/qgroundcontrol/QGCActivity.java b/android/src/org/mavlink/qgroundcontrol/QGCActivity.java index a716392e6eacc4a9a920f2d9cc0f70533a63a836..f76242f5e67adfc71d7e3d5d362f693c0faf5ff4 100644 --- a/android/src/org/mavlink/qgroundcontrol/QGCActivity.java +++ b/android/src/org/mavlink/qgroundcontrol/QGCActivity.java @@ -29,11 +29,14 @@ package org.mavlink.qgroundcontrol; // //////////////////////////////////////////////////////////////////////////////////////////// +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.ArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.Timer; +import java.util.TimerTask; import java.io.IOException; import android.app.Activity; @@ -42,12 +45,16 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.hardware.usb.*; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; import android.widget.Toast; import android.util.Log; import android.os.PowerManager; -import android.view.WindowManager; import android.os.Bundle; +import android.app.PendingIntent; +import android.view.WindowManager; import com.hoho.android.usbserial.driver.*; import org.qtproject.qt5.android.bindings.QtActivity; @@ -63,8 +70,10 @@ public class QGCActivity extends QtActivity private static HashMap _userDataHashByDeviceId; private static final String TAG = "QGC_QGCActivity"; private static PowerManager.WakeLock _wakeLock; - private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; +// private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; + private static final String ACTION_USB_PERMISSION = "org.mavlink.qgroundcontrol.action.USB_PERMISSION"; private static PendingIntent _usbPermissionIntent = null; + private TaiSync taiSync = null; public static Context m_context; @@ -87,6 +96,26 @@ public class QGCActivity extends QtActivity } }; + private final BroadcastReceiver mOpenAccessoryReceiver = + new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (ACTION_USB_PERMISSION.equals(action)) { + UsbAccessory accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY); + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + openAccessory(accessory); + } + } else if( UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { + UsbAccessory accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY); + if (accessory != null) { + closeAccessory(accessory); + } + } + } + }; + private static UsbSerialDriver _findDriverByDeviceId(int deviceId) { for (UsbSerialDriver driver: _drivers) { if (driver.getDevice().getDeviceId() == deviceId) { @@ -169,7 +198,7 @@ public class QGCActivity extends QtActivity _usbManager = (UsbManager)_instance.getSystemService(Context.USB_SERVICE); - // Register for USB Detach and USB Permission intent + // Register for USB Detach and USB Permission intent IntentFilter filter = new IntentFilter(); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); filter.addAction(ACTION_USB_PERMISSION); @@ -177,11 +206,24 @@ public class QGCActivity extends QtActivity // Create intent for usb permission request _usbPermissionIntent = PendingIntent.getBroadcast(_instance, 0, new Intent(ACTION_USB_PERMISSION), 0); + + try { + taiSync = new TaiSync(); + + IntentFilter accessoryFilter = new IntentFilter(ACTION_USB_PERMISSION); + filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); + registerReceiver(mOpenAccessoryReceiver, accessoryFilter); + + probeAccessories(); + } catch(Exception e) { + Log.e(TAG, "Exception: " + e); + } } @Override protected void onDestroy() { + unregisterReceiver(mOpenAccessoryReceiver); try { if(_wakeLock != null) { _wakeLock.release(); @@ -611,5 +653,61 @@ public class QGCActivity extends QtActivity else return connectL.getFileDescriptor(); } + + UsbAccessory openUsbAccessory = null; + Object openAccessoryLock = new Object(); + + private void openAccessory(UsbAccessory usbAccessory) + { + Log.i(TAG, "openAccessory: " + usbAccessory.getSerial()); + try { + synchronized(openAccessoryLock) { + if ((openUsbAccessory != null && !taiSync.isRunning()) || openUsbAccessory == null) { + openUsbAccessory = usbAccessory; + taiSync.open(_usbManager.openAccessory(usbAccessory)); + } + } + } catch (IOException e) { + Log.e(TAG, "openAccessory exception: " + e); + taiSync.close(); + closeAccessory(openUsbAccessory); + } + } + + private void closeAccessory(UsbAccessory usbAccessory) + { + Log.i(TAG, "closeAccessory"); + + synchronized(openAccessoryLock) { + if (openUsbAccessory != null && usbAccessory == openUsbAccessory && taiSync.isRunning()) { + taiSync.close(); + openUsbAccessory = null; + } + } + } + + private void probeAccessories() + { + final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() + { +// Log.i(TAG, "probeAccessories"); + UsbAccessory[] accessories = _usbManager.getAccessoryList(); + if (accessories != null) { + for (UsbAccessory usbAccessory : accessories) { + if (_usbManager.hasPermission(usbAccessory)) { + openAccessory(usbAccessory); + } else { + Log.i(TAG, "requestPermission"); + _usbManager.requestPermission(usbAccessory, pendingIntent); + } + } + } + } + }, 0, 3000); + } } diff --git a/android/src/org/mavlink/qgroundcontrol/TaiSync.java b/android/src/org/mavlink/qgroundcontrol/TaiSync.java new file mode 100644 index 0000000000000000000000000000000000000000..1d746c94a4db25f95b5924b0f9e1d905f77d9ed2 --- /dev/null +++ b/android/src/org/mavlink/qgroundcontrol/TaiSync.java @@ -0,0 +1,286 @@ +package org.mavlink.qgroundcontrol; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.Socket; +import java.net.InetAddress; + +import android.os.ParcelFileDescriptor; +import android.util.Log; + +public class TaiSync +{ + private static final int HEADER_SIZE = 0x1C; + + private static final byte PROTOCOL_REQUEST_CONNECTION = 0x00; + private static final byte PROTOCOL_VERSION = 0x01; + private static final byte PROTOCOL_CHANNEL = 0x02; + private static final byte PROTOCOL_DATA = 0x03; + + private static final int VIDEO_PORT = 5600; + private static final int TAISYNC_VIDEO_PORT = 8000; + private static final int TAISYNC_SETTINGS_PORT = 8200; + private static final int TAISYNC_TELEMETRY_PORT = 8400; + + private Object runLock; + private boolean running = false; + private DatagramSocket udpSocket = null; + private Socket tcpSettingsSocket = null; + private InputStream settingsInStream = null; + private OutputStream settingsOutStream = null; + private Socket tcpTelemetrySocket = null; + private InputStream telemetryInStream = null; + private OutputStream telemetryOutStream = null; + private ParcelFileDescriptor mParcelFileDescriptor; + private FileInputStream mFileInputStream; + private FileOutputStream mFileOutputStream; + private ExecutorService mThreadPool; + private byte[] mBytes = new byte[32 * 1024]; + private byte vMaj = 0; + + public TaiSync() + { + runLock = new Object(); + mThreadPool = Executors.newFixedThreadPool(3); + } + + public boolean isRunning() + { + synchronized (runLock) { + return running; + } + } + + public void open(ParcelFileDescriptor parcelFileDescriptor) throws IOException + { +// Log.i("QGC_TaiSync", "Open"); + + synchronized (runLock) { + if (running) { + return; + } + running = true; + } + + mParcelFileDescriptor = parcelFileDescriptor; + if (mParcelFileDescriptor == null) { + throw new IOException("parcelFileDescriptor is null"); + } + + FileDescriptor fileDescriptor = mParcelFileDescriptor.getFileDescriptor(); + mFileInputStream = new FileInputStream(fileDescriptor); + mFileOutputStream = new FileOutputStream(fileDescriptor); + + udpSocket = new DatagramSocket(); + final InetAddress address = InetAddress.getByName("localhost"); + tcpTelemetrySocket = new Socket(address, TAISYNC_TELEMETRY_PORT); + telemetryInStream = tcpTelemetrySocket.getInputStream(); + telemetryOutStream = tcpTelemetrySocket.getOutputStream(); + tcpSettingsSocket = new Socket(address, TAISYNC_SETTINGS_PORT); + settingsInStream = tcpSettingsSocket.getInputStream(); + settingsOutStream = tcpSettingsSocket.getOutputStream(); + + // Request connection packet + sendTaiSyncMessage(PROTOCOL_REQUEST_CONNECTION, 0, null, 0); + + // Read multiplexed data stream coming from TaiSync accessory + mThreadPool.execute(new Runnable() { + @Override + public void run() + { + int bytesRead = 0; + + try { + while (bytesRead >= 0) { + synchronized (runLock) { + if (!running) { + break; + } + } + + bytesRead = mFileInputStream.read(mBytes); + + if (bytesRead > 0) + { + if (mBytes[3] == PROTOCOL_VERSION) + { + vMaj = mBytes[19]; + Log.i("QGC_TaiSync", "Got protocol version message vMaj = " + mBytes[19]); + sendTaiSyncMessage(PROTOCOL_VERSION, 0, null, 0); + } + else if (mBytes[3] == PROTOCOL_CHANNEL) { + int dPort = ((mBytes[4] & 0xff)<< 24) | ((mBytes[5]&0xff) << 16) | ((mBytes[6]&0xff) << 8) | (mBytes[7] &0xff); + int dLength = ((mBytes[8] & 0xff)<< 24) | ((mBytes[9]&0xff) << 16) | ((mBytes[10]&0xff) << 8) | (mBytes[11] &0xff); + Log.i("QGC_TaiSync", "Read 2 port = " + dPort + " length = " + dLength); + sendTaiSyncMessage(PROTOCOL_CHANNEL, dPort, null, 0); + } + else if (mBytes[3] == PROTOCOL_DATA) { + int dPort = ((mBytes[4] & 0xff)<< 24) | ((mBytes[5]&0xff) << 16) | ((mBytes[6]&0xff) << 8) | (mBytes[7] &0xff); + int dLength = ((mBytes[8] & 0xff)<< 24) | ((mBytes[9]&0xff) << 16) | ((mBytes[10]&0xff) << 8) | (mBytes[11] &0xff); + + int payloadOffset = HEADER_SIZE; + int payloadLength = bytesRead - payloadOffset; + + byte[] sBytes = new byte[payloadLength]; + System.arraycopy(mBytes, payloadOffset, sBytes, 0, payloadLength); + + if (dPort == TAISYNC_VIDEO_PORT) { + DatagramPacket packet = new DatagramPacket(sBytes, sBytes.length, address, VIDEO_PORT); + udpSocket.send(packet); + } else if (dPort == TAISYNC_SETTINGS_PORT) { + settingsOutStream.write(sBytes); + } else if (dPort == TAISYNC_TELEMETRY_PORT) { + telemetryOutStream.write(sBytes); + } + } + } + } + } catch (IOException e) { + Log.e("QGC_TaiSync", "Exception: " + e); + e.printStackTrace(); + } finally { + close(); + } + } + }); + + // Read command & control packets to be sent to telemetry port + mThreadPool.execute(new Runnable() { + @Override + public void run() + { + byte[] inbuf = new byte[256]; + + try { + while (true) { + synchronized (runLock) { + if (!running) { + break; + } + } + + int bytesRead = telemetryInStream.read(inbuf, 0, inbuf.length); + if (bytesRead > 0) { + sendTaiSyncMessage(PROTOCOL_DATA, TAISYNC_TELEMETRY_PORT, inbuf, bytesRead); + } + } + } catch (IOException e) { + Log.e("QGC_TaiSync", "Exception: " + e); + e.printStackTrace(); + } finally { + close(); + } + } + }); + + // Read incoming requests for settings socket + mThreadPool.execute(new Runnable() { + @Override + public void run() + { + byte[] inbuf = new byte[1024]; + + try { + while (true) { + synchronized (runLock) { + if (!running) { + break; + } + } + + int bytesRead = settingsInStream.read(inbuf, 0, inbuf.length); + if (bytesRead > 0) { + sendTaiSyncMessage(PROTOCOL_DATA, TAISYNC_SETTINGS_PORT, inbuf, bytesRead); + } + } + } catch (IOException e) { + Log.e("QGC_TaiSync", "Exception: " + e); + e.printStackTrace(); + } finally { + close(); + } + } + }); + } + + private void sendTaiSyncMessage(byte protocol, int dataPort, byte[] data, int dataLen) throws IOException + { + byte portMSB = (byte)((dataPort >> 8) & 0xFF); + byte portLSB = (byte)(dataPort & 0xFF); + + byte[] lA = new byte[4]; + int len = HEADER_SIZE + dataLen; + Log.i("QGC_TaiSync", "Sending to " + dataPort + " length = " + len); + byte[] buffer = new byte[len]; + + for (int i = 3; i >= 0; i--) { + lA[i] = (byte)(len & 0xFF); + len >>= 8; + } + + byte[] header = { 0x00, 0x00, 0x00, protocol, // uint32 - protocol + 0x00, 0x00, portMSB, portLSB, // uint32 - dport + lA[0], lA[1], lA[2], lA[3], // uint32 - length + 0x00, 0x00, 0x00, 0x00, // uint32 - magic + 0x00, 0x00, 0x00, vMaj, // uint32 - version major + 0x00, 0x00, 0x00, 0x00, // uint32 - version minor + 0x00, 0x00, 0x00, 0x00 }; // uint32 - padding + + System.arraycopy(header, 0, buffer, 0, header.length); + if (data != null && dataLen > 0) { + System.arraycopy(data, 0, buffer, header.length, dataLen); + } + + synchronized (runLock) { + mFileOutputStream.write(buffer); + } + } + + public void close() + { +// Log.i("QGC_TaiSync", "Close"); + synchronized (runLock) { + running = false; + } + try { + if (udpSocket != null) { + udpSocket.close(); + } + } catch (Exception e) { + } + try { + if (tcpTelemetrySocket != null) { + tcpTelemetrySocket.close(); + } + } catch (Exception e) { + } + try { + if (tcpSettingsSocket != null) { + tcpSettingsSocket.close(); + } + } catch (Exception e) { + } + try { + if (mParcelFileDescriptor != null) { + mParcelFileDescriptor.close(); + } + } catch (Exception e) { + } + udpSocket = null; + tcpSettingsSocket = null; + tcpTelemetrySocket = null; + settingsInStream = null; + settingsOutStream = null; + mParcelFileDescriptor = null; + } +} diff --git a/src/VideoStreaming/README.md b/src/VideoStreaming/README.md index eb8d8f45c6936ee13c23a4d38b762d94401e98d2..6d1451645f2fa2e7d7a804418a7ad0df21f584ab 100644 --- a/src/VideoStreaming/README.md +++ b/src/VideoStreaming/README.md @@ -70,16 +70,19 @@ The installer places them under ~/Library/Developer/GStreamer/iPhone.sdk/GStream ### Android Binaries found in http://gstreamer.freedesktop.org/data/pkg/android -Download the [gstreamer-1.0-android-armv7-1.5.2.tar.bz2](http://gstreamer.freedesktop.org/data/pkg/android/1.5.2/gstreamer-1.0-android-armv7-1.5.2.tar.bz2) archive (assuming you want the ARM V7 platform. For x86, replace `armv7` with `x86` accordingly). +Download the [gstreamer-1.0-android-universal-1.14.4.tar.bz2](https://gstreamer.freedesktop.org/data/pkg/android/1.14.4/gstreamer-1.0-android-universal-1.14.4.tar.bz2) archive. -Create a directory named "gstreamer-1.0-android-armv7-1.5.2" under the root qgroundcontrol directory (the same directory qgroundcontrol.pro is located). Extract the gstreamer tar file under this directory. That's where the build system will look for it. Make sure your archive tool doesn't create any additional top level directories. The structure after extracting the archive should look like this: +Create a directory named "gstreamer-1.0-android-universal-1.14.4" under the root qgroundcontrol directory (the same directory qgroundcontrol.pro is located). Extract the downloaded archive under this directory. That's where the build system will look for it. Make sure your archive tool doesn't create any additional top level directories. The structure after extracting the archive should look like this: ``` qgroundcontrol -├── gstreamer-1.0-android-armv7-1.5.2 -│   ├── etc -│   ├── include -│   ├── lib -│   └── share +├── gstreamer-1.0-android-universal-1.14.4 +│ │ +│   ├──armv7 +│   │   ├── etc +│   │   ├── include +│   │   ├── lib +│   │   └── share +│   ├──x86 ``` ### Windows diff --git a/src/VideoStreaming/VideoReceiver.cc b/src/VideoStreaming/VideoReceiver.cc index ed750ce71e813ad1b49fa2848341646f9343e6f5..22fcf2b115077a4b101f127e4a8a5a00fa790124 100644 --- a/src/VideoStreaming/VideoReceiver.cc +++ b/src/VideoStreaming/VideoReceiver.cc @@ -539,7 +539,8 @@ VideoReceiver::_shutdownPipeline() { void VideoReceiver::_handleError() { qCDebug(VideoReceiverLog) << "Gstreamer error!"; - _shutdownPipeline(); + stop(); + start(); } #endif @@ -554,7 +555,8 @@ VideoReceiver::_handleEOS() { _shutdownRecordingBranch(); } else { qWarning() << "VideoReceiver: Unexpected EOS!"; - _shutdownPipeline(); + stop(); + start(); } } #endif