diff --git a/android-mobile/src/org/subsurfacedivelog/mobile/AndroidSerial.java b/android-mobile/src/org/subsurfacedivelog/mobile/AndroidSerial.java new file mode 100644 index 000000000..88b167fda --- /dev/null +++ b/android-mobile/src/org/subsurfacedivelog/mobile/AndroidSerial.java @@ -0,0 +1,287 @@ +package org.subsurfacedivelog.mobile; + +import com.hoho.android.usbserial.driver.UsbSerialDriver; +import com.hoho.android.usbserial.driver.UsbSerialPort; +import com.hoho.android.usbserial.driver.UsbSerialProber; + +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; +import android.hardware.usb.UsbDevice; + +import android.content.Context; +import android.util.Log; +import android.app.PendingIntent; +import android.content.Intent; + +import org.subsurfacedivelog.mobile.SubsurfaceMobileActivity; + +import java.lang.System; +import java.lang.Thread; +import java.util.Queue; +import java.util.List; +import java.util.LinkedList; +import java.lang.Math; + +public class AndroidSerial { + + private static final String TAG = "AndroidSerial"; + + private static final int DC_STATUS_SUCCESS = 0; + private static final int DC_STATUS_DONE = 1; + private static final int DC_STATUS_UNSUPPORTED = -1; + private static final int DC_STATUS_INVALIDARGS = -2; + private static final int DC_STATUS_NOMEMORY = -3; + private static final int DC_STATUS_NODEVICE = -4; + private static final int DC_STATUS_NOACCESS = -5; + private static final int DC_STATUS_IO = -6; + private static final int DC_STATUS_TIMEOUT = -7; + private static final int DC_STATUS_PROTOCOL = -8; + private static final int DC_STATUS_DATAFORMAT = -9; + private static final int DC_STATUS_CANCELLED = -1; + + /** + * The parity checking scheme. + * Matches us-serial-for-android + */ + private static final int DC_PARITY_NONE = 0; + private static final int DC_PARITY_ODD = 1; + private static final int DC_PARITY_EVEN = 2; + private static final int DC_PARITY_MARK = 3; + private static final int DC_PARITY_SPACE = 4; + + /** + * The number of stop bits. + * Has to be translated for usb-serial-for-android + */ + private static final int DC_STOPBITS_ONE = 0; /**< 1 stop bit */ + private static final int DC_STOPBITS_ONEPOINTFIVE = 1; /**< 1.5 stop bits*/ + private static final int DC_STOPBITS_TWO = 2; /**< 2 stop bits */ + + /** + * The direction of the data transmission. + */ + private static final int DC_DIRECTION_INPUT = 1; /**< Input direction */ + private static final int DC_DIRECTION_OUTPUT = 2; /**< Output direction */ + private static final int DC_DIRECTION_ALL = 3; /**< All directions */ + + + private UsbSerialPort usbSerialPort; + private int timeout = 0; + private LinkedList readBuffer = new LinkedList(); + + private static String printQueue(LinkedList readBuffer) + { + String str = "" + readBuffer.size() + " elements: "; + for (byte element : readBuffer) { + str = str + String.format("%02X", element); + } + return str; + } + + + private AndroidSerial(UsbSerialPort usbSerialPort) + { + this.usbSerialPort = usbSerialPort; + } + + public static AndroidSerial open_android_serial() + { + try { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + // Find all available drivers from attached devices. + Context context = SubsurfaceMobileActivity.getAppContext(); + UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + UsbSerialProber usbSerialProber = UsbSerialProber.getDefaultProber(); + + // TODO attach custom VID / PID / Drivers + + List availableDrivers = usbSerialProber.findAllDrivers(manager); + if (availableDrivers.isEmpty()) { + Log.w(TAG, "no usb-to-serial devices found!"); + return null; + } + + // Open a connection to the first available driver. + UsbSerialDriver driver = availableDrivers.get(0); + UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); + + if (connection == null) { + manager.requestPermission(driver.getDevice(), PendingIntent.getBroadcast(context, 0, new Intent("org.subsurfacedivelog.mobile.USB_PERMISSION"), 0)); + Log.w(TAG, "Could not open device!"); + return null; + } + + Log.i(TAG, "Num ports: " + driver.getPorts().size()); + + UsbSerialPort usbSerialPort = driver.getPorts().get(0); // Most devices have just one port (port 0) + usbSerialPort.open(connection); + Log.i(TAG, "Opened serial device " + usbSerialPort); + + return new AndroidSerial(usbSerialPort); + + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return null; + } + } + + /* dc_status_t */ + public int set_timeout(int timeout) + { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + this.timeout = timeout; + return AndroidSerial.DC_STATUS_SUCCESS; + } + + /* dc_status_t */ + public int set_dtr(boolean value) + { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + try { + usbSerialPort.setDTR(value); + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return AndroidSerial.DC_STATUS_IO; + } + return AndroidSerial.DC_STATUS_SUCCESS; + } + + /* dc_status_t */ + public int set_rts(boolean value) + { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + try { + usbSerialPort.setRTS(value); + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return AndroidSerial.DC_STATUS_IO; + } + return AndroidSerial.DC_STATUS_SUCCESS; + } + + /* length of read data, or dc_status_t if not successful */ + /* + // Currently commented out since a non-blocking get_available is not possible due to usb-serial-for-android limitations + public int get_available() { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + try { + byte[] readData = new byte[64]; // TODO magic number + int len = usbSerialPort.read(readData, 250); // TODO magic number + for (int i = 0; i < len; i++) { + readBuffer.add(readData[i]); + } + return readBuffer.size(); + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return AndroidSerial.DC_STATUS_IO; + } + } + */ + + // Flow control is not implemented in usb-serial-for-android. It's not used by any dive computer anyways... + /* dc_status_t */ + public int configure(int baudrate, int databits, /*dc_parity_t*/ int parity, /*dc_stopbits_t*/ int stopbits) + { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName() + ", baudrate=" + baudrate + ", databits=" + databits + ", parity=" + parity + ", stopbits=" + stopbits); + int translated_stopbits = 0; + switch(stopbits) { + case AndroidSerial.DC_STOPBITS_ONE: + translated_stopbits = UsbSerialPort.STOPBITS_1; + break; + case AndroidSerial.DC_STOPBITS_ONEPOINTFIVE: + translated_stopbits = UsbSerialPort.STOPBITS_1_5; + break; + case AndroidSerial.DC_STOPBITS_TWO: + translated_stopbits = UsbSerialPort.STOPBITS_2; + break; + } + + try { + usbSerialPort.setParameters(baudrate, databits, translated_stopbits, parity); + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return AndroidSerial.DC_STATUS_IO; + } + return AndroidSerial.DC_STATUS_SUCCESS; + } + + /* length of read data, or dc_status_t if not successful */ + public int read(byte[] data) + { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + + try { + + Log.d(TAG, "read length: " + data.length); + + int toReadFromHwLength = data.length - readBuffer.size(); + + int arraylength = (toReadFromHwLength % 64) != 0 ? toReadFromHwLength + (64 - (toReadFromHwLength % 64)): toReadFromHwLength; // use blocks of 64 for reading + + // When we don't have enough in the buffer, try to read from HW + if (toReadFromHwLength > 0) { + // Read and append to buffer + byte[] readFromHwData = new byte[arraylength]; + int actuallyReadFromHwLength = usbSerialPort.read(readFromHwData, 0); // With this it works... But the timeout is ignored! Fix this! + for (int i = 0; i < actuallyReadFromHwLength; i++ ) { + readBuffer.add(readFromHwData[i]); + } + } + + //Log.d(TAG, "read buffer: " + printQueue(readBuffer)); + int returnLength = 0; + for (returnLength = 0; returnLength < data.length; returnLength++) { + if (readBuffer.isEmpty()) break; + data[returnLength] = readBuffer.remove(); + } + + Log.d(TAG, "return length: " + returnLength); + return returnLength; + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return AndroidSerial.DC_STATUS_IO; + } + } + + /* number of bytes written, or dc_status_t */ + public int write (byte[] data) + { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + try { + Log.d(TAG, "write length: " + data.length); + int retval = usbSerialPort.write(data, timeout); + Log.d(TAG, "actual write length: " + retval); + return retval; + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return AndroidSerial.DC_STATUS_IO; + } + } + + /* dc_status_t */ + public int purge (/*dc_direction_t*/ int direction) + { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + try { + if ((direction | AndroidSerial.DC_DIRECTION_INPUT) > 0) readBuffer.clear(); + boolean retval = this.usbSerialPort.purgeHwBuffers((direction | AndroidSerial.DC_DIRECTION_OUTPUT) > 0, (direction | AndroidSerial.DC_DIRECTION_INPUT) > 0); + return retval ? AndroidSerial.DC_STATUS_SUCCESS : AndroidSerial.DC_STATUS_IO; + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return AndroidSerial.DC_STATUS_IO; + } + } + + /*dc_status_t*/ + public int close () + { + Log.d(TAG, "in " + Thread.currentThread().getStackTrace()[2].getMethodName()); + try { + usbSerialPort.close(); + return AndroidSerial.DC_STATUS_SUCCESS; + } catch (Exception e) { + Log.e(TAG, "Error in " + Thread.currentThread().getStackTrace()[2].getMethodName(), e); + return AndroidSerial.DC_STATUS_IO; + } + } +} diff --git a/android-mobile/src/org/subsurfacedivelog/mobile/SubsurfaceMobileActivity.java b/android-mobile/src/org/subsurfacedivelog/mobile/SubsurfaceMobileActivity.java index 4c5bc576b..c909210f2 100644 --- a/android-mobile/src/org/subsurfacedivelog/mobile/SubsurfaceMobileActivity.java +++ b/android-mobile/src/org/subsurfacedivelog/mobile/SubsurfaceMobileActivity.java @@ -28,15 +28,19 @@ public class SubsurfaceMobileActivity extends QtActivity public static boolean isInitialized; private static final String TAG = "subsurfacedivelog.mobile"; public static native void setDeviceString(String deviceString); + private static Context appContext; // we need to provide two endpoints: // onNewIntent if we receive an Intent while running // onCreate if we were started by an Intent @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) + { Log.i(TAG + " onCreate", "onCreate SubsurfaceMobileActivity"); super.onCreate(savedInstanceState); + appContext = getApplicationContext(); + // now we're checking if the App was started from another Android App via Intent Intent theIntent = getIntent(); if (theIntent != null) { @@ -50,7 +54,8 @@ public class SubsurfaceMobileActivity extends QtActivity // if we are opened from other apps: @Override - public void onNewIntent(Intent intent) { + public void onNewIntent(Intent intent) + { Log.i(TAG + " onNewIntent", intent.getAction()); UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device == null) { @@ -68,7 +73,8 @@ public class SubsurfaceMobileActivity extends QtActivity } } // onNewIntent - public void checkPendingIntents() { + public void checkPendingIntents() + { isInitialized = true; if (isIntentPending) { isIntentPending = false; @@ -80,7 +86,8 @@ public class SubsurfaceMobileActivity extends QtActivity } // checkPendingIntents - private void processIntent() { + private void processIntent() + { Intent intent = getIntent(); if (intent == null) { Log.i(TAG + " processIntent", "intent is null"); @@ -95,4 +102,10 @@ public class SubsurfaceMobileActivity extends QtActivity Log.i(TAG + " processIntent device name", device.getDeviceName()); setDeviceString(device.toString()); } // processIntent + + + public static Context getAppContext() + { + return appContext; + } } diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index d3f1401f1..58ffa67e4 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -2,12 +2,12 @@ set(PLATFORM_SRC unknown_platform.c) message(STATUS "system name ${CMAKE_SYSTEM_NAME}") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") if(ANDROID) - set(PLATFORM_SRC android.cpp) + set(PLATFORM_SRC android.cpp serial_usb_android.cpp) else() set(PLATFORM_SRC unix.c) endif() elseif(CMAKE_SYSTEM_NAME STREQUAL "Android") - set(PLATFORM_SRC android.cpp) + set(PLATFORM_SRC android.cpp serial_usb_android.cpp) elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(PLATFORM_SRC macos.c) elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") diff --git a/core/libdivecomputer.c b/core/libdivecomputer.c index 453bb4e2f..79eb63c7f 100644 --- a/core/libdivecomputer.c +++ b/core/libdivecomputer.c @@ -1318,6 +1318,10 @@ dc_status_t divecomputer_device_open(device_data_t *data) #ifdef SERIAL_FTDI if (!strcasecmp(data->devname, "ftdi")) return ftdi_open(&data->iostream, context); +#endif +#ifdef __ANDROID__ + if (!strcasecmp(data->devname, "usb-serial")) + return serial_usb_android_open(&data->iostream, context); #endif rc = dc_serial_open(&data->iostream, context, data->devname); if (rc == DC_STATUS_SUCCESS) diff --git a/core/libdivecomputer.h b/core/libdivecomputer.h index 98456112a..e24143a7b 100644 --- a/core/libdivecomputer.h +++ b/core/libdivecomputer.h @@ -67,6 +67,7 @@ extern char *dumpfile_name; dc_status_t ble_packet_open(dc_iostream_t **iostream, dc_context_t *context, const char* devaddr, void *userdata); dc_status_t rfcomm_stream_open(dc_iostream_t **iostream, dc_context_t *context, const char* devaddr); dc_status_t ftdi_open(dc_iostream_t **iostream, dc_context_t *context); +dc_status_t serial_usb_android_open(dc_iostream_t **iostream, dc_context_t *context); dc_status_t divecomputer_device_open(device_data_t *data); diff --git a/core/serial_usb_android.cpp b/core/serial_usb_android.cpp new file mode 100644 index 000000000..a5db132ae --- /dev/null +++ b/core/serial_usb_android.cpp @@ -0,0 +1,197 @@ +#include "libdivecomputer.h" +#include +#include + +#include + +#include +#include + +#include + +#include + +#define INFO(context, fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, __FILE__, "INFO: " fmt "\n", ##__VA_ARGS__) +#define ERROR(context, fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, __FILE__, "ERROR: " fmt "\n", ##__VA_ARGS__) +#define TRACE INFO + +static dc_status_t serial_usb_android_sleep(void *io, unsigned int timeout) +{ + TRACE (device->context, "%s: %i", __FUNCTION__, timeout); + + QAndroidJniObject *device = static_cast(io); + if (device == nullptr) + return DC_STATUS_INVALIDARGS; + + std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + return DC_STATUS_SUCCESS; +} + +static dc_status_t serial_usb_android_set_timeout(void *io, int timeout) +{ + TRACE (device->context, "%s: %i", __FUNCTION__, timeout); + + QAndroidJniObject *device = static_cast(io); + if (device == nullptr) + return DC_STATUS_INVALIDARGS; + + return static_cast(device->callMethod("set_timeout", "(I)I", timeout)); +} + +static dc_status_t serial_usb_android_set_dtr(void *io, unsigned int value) +{ + TRACE (device->context, "%s: %i", __FUNCTION__, value); + + QAndroidJniObject *device = static_cast(io); + if (device == nullptr) + return DC_STATUS_INVALIDARGS; + + return static_cast(device->callMethod("set_dtr", "(Z)I", value)); +} + +static dc_status_t serial_usb_android_set_rts(void *io, unsigned int value) +{ + TRACE (device->context, "%s: %i", __FUNCTION__, value); + + QAndroidJniObject *device = static_cast(io); + if (device == nullptr) + return DC_STATUS_INVALIDARGS; + + return static_cast(device->callMethod("set_rts", "(Z)I", value)); +} + +static dc_status_t serial_usb_android_close(void *io) +{ + TRACE (device->context, "%s", __FUNCTION__); + + QAndroidJniObject *device = static_cast(io); + if (device == nullptr) + return DC_STATUS_SUCCESS; + + auto retval = static_cast(device->callMethod("close", "()I")); + delete device; + return retval; +} + +static dc_status_t serial_usb_android_purge(void *io, dc_direction_t direction) +{ + TRACE (device->context, "%s: %i", __FUNCTION__, direction); + + QAndroidJniObject *device = static_cast(io); + if (device == nullptr) + return DC_STATUS_INVALIDARGS; + + return static_cast(device->callMethod("purge", "(I)I", direction)); +} + +static dc_status_t +serial_usb_android_configure(void *io, unsigned int baudrate, unsigned int databits, dc_parity_t parity, + dc_stopbits_t stopbits, dc_flowcontrol_t flowcontrol) +{ + TRACE (device->context, "%s: baudrate=%i, databits=%i, parity=%i, stopbits=%i, flowcontrol=%i", __FUNCTION__, + baudrate, databits, parity, stopbits, flowcontrol); + + QAndroidJniObject *device = static_cast(io); + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + return static_cast(device->callMethod("configure", "(IIII)I", baudrate, databits, parity, stopbits)); +} + +/* +static dc_status_t serial_usb_android_get_available (void *io, size_t *value) +{ + INFO (device->context, "%s", __FUNCTION__); + + QAndroidJniObject *device = static_cast(io); + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + + auto retval = device->callMethod("get_available", "()I"); + if(retval < 0){ + INFO (device->context, "Error in %s, retval %i", __FUNCTION__, retval); + return static_cast(retval); + } + + *value = retval; + return DC_STATUS_SUCCESS; +} +*/ + +static dc_status_t serial_usb_android_read(void *io, void *data, size_t size, size_t *actual) +{ + TRACE (device->context, "%s: size: %i", __FUNCTION__, size); + + QAndroidJniObject *device = static_cast(io); + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + QAndroidJniEnvironment env; + jbyteArray array = env->NewByteArray(size); + env->SetByteArrayRegion(array, 0, size, (const jbyte *) data); + + auto retval = device->callMethod("read", "([B)I", array); + if (retval < 0) { + env->DeleteLocalRef(array); + INFO (device->context, "Error in %s, retval %i", __FUNCTION__, retval); + return static_cast(retval); + } + *actual = retval; + env->GetByteArrayRegion(array, 0, retval, (jbyte *) data); + env->DeleteLocalRef(array); + TRACE (device->context, "%s: actual read size: %i", __FUNCTION__, retval); + return DC_STATUS_SUCCESS; +} + + +static dc_status_t serial_usb_android_write(void *io, const void *data, size_t size, size_t *actual) +{ + TRACE (device->context, "%s: size: %i", __FUNCTION__, size); + + QAndroidJniObject *device = static_cast(io); + if (device == NULL) + return DC_STATUS_INVALIDARGS; + + QAndroidJniEnvironment env; + jbyteArray array = env->NewByteArray(size); + env->SetByteArrayRegion(array, 0, size, (const jbyte *) data); + + auto retval = device->callMethod("write", "([B)I", array); + env->DeleteLocalRef(array); + if (retval < 0) { + INFO (device->context, "Error in %s, retval %i", __FUNCTION__, retval); + return static_cast(retval); + } + *actual = retval; + TRACE (device->context, "%s: actual write size: %i", __FUNCTION__, retval); + return DC_STATUS_SUCCESS; +} + +dc_status_t serial_usb_android_open(dc_iostream_t **iostream, dc_context_t *context) +{ + TRACE(device->contxt, "%s", __FUNCTION__); + + static const dc_custom_cbs_t callbacks = { + .set_timeout = serial_usb_android_set_timeout, /* set_timeout */ + .set_dtr = serial_usb_android_set_dtr, /* set_dtr */ + .set_rts = serial_usb_android_set_rts, /* set_rts */ + //.get_available = serial_usb_android_get_available, + .configure = serial_usb_android_configure, + .read = serial_usb_android_read, + .write = serial_usb_android_write, + .purge = serial_usb_android_purge, + .sleep = serial_usb_android_sleep, /* sleep */ + .close = serial_usb_android_close, /* close */ + }; + + QAndroidJniObject localdevice = QAndroidJniObject::callStaticObjectMethod("org/subsurfacedivelog/mobile/AndroidSerial", + "open_android_serial", + "()Lorg/subsurfacedivelog/mobile/AndroidSerial;"); + if (localdevice == nullptr) { + return DC_STATUS_IO; + } + QAndroidJniObject *device = new QAndroidJniObject(localdevice); + TRACE(device->contxt, "%s", "calling dc_custom_open())"); + return dc_custom_open(iostream, context, DC_TRANSPORT_SERIAL, &callbacks, device); +} diff --git a/subsurface-mobile-main.cpp b/subsurface-mobile-main.cpp index 7b7269f85..9529682df 100644 --- a/subsurface-mobile-main.cpp +++ b/subsurface-mobile-main.cpp @@ -88,10 +88,8 @@ int main(int argc, char **argv) void set_non_bt_addresses() { -#if SERIAL_FTDI - connectionListModel.addAddress("FTDI"); -#endif #if defined(Q_OS_ANDROID) + connectionListModel.addAddress("usb-serial"); #elif defined(Q_OS_LINUX) // since this is in the else, it does NOT include Android connectionListModel.addAddress("/dev/ttyS0"); connectionListModel.addAddress("/dev/ttyS1"); @@ -100,6 +98,9 @@ void set_non_bt_addresses() // this makes debugging so much easier - use the simulator connectionListModel.addAddress("/tmp/ttyS1"); #endif +#if defined(SERIAL_FTDI) + connectionListModel.addAddress("FTDI"); +#endif } bool haveFilesOnCommandLine()