Using serial port on android
-
Hi,
I am porting a Qt application to Android. A major feature of the application relies on using the QSerialPort module, which is not officially supported by Qt Android and it only works on rooted devices which is not a solution for me. Is there a way to port my application without having to make large changes to my code (like switching to BT instead of QSerialPort)?I have seen a patch mentioned before, I think it is this one, https://codereview.qt-project.org/c/qt/qtserialport/+/84338/2 . Is this still the way to make QSerialPort work in Qt 5.15.15 or is there another way?
Kind Regards
Chaz. -
Hi,
I am porting a Qt application to Android. A major feature of the application relies on using the QSerialPort module, which is not officially supported by Qt Android and it only works on rooted devices which is not a solution for me. Is there a way to port my application without having to make large changes to my code (like switching to BT instead of QSerialPort)?I have seen a patch mentioned before, I think it is this one, https://codereview.qt-project.org/c/qt/qtserialport/+/84338/2 . Is this still the way to make QSerialPort work in Qt 5.15.15 or is there another way?
Kind Regards
Chaz.I had the same issue for an FTDI usb device and ended up re-writing the interface with JNI.
For the android part I used this package:
https://github.com/mik3y/usb-serial-for-android/For this package I had to switch to OpenJDK 18, in order to use the required gradle version.
As the JNI part is static, I used the singleton pattern to build an asynchronous interface.
Depending on your current implementation / usage of QSerialPort, you might be able to simply swap out the QSerialPort for a JNI wrapper class on Android.
In total this looks something like this:
- mydevice.h
- mydevice.cpp
- android/src/com/mycompany/mydevice/SerialHelper.java
- changes in AndroidManifest.xml
- changes in build.gradle
In the following some snippets. I tried to reduce the snippets but to still cover the JNI parts.
// mydevice.h // JNI methods public: static void JNICALL javaResponseReady(JNIEnv *env, jobject obj, jbyteArray byteArray); static void JNICALL javaConnectedStateChanged(JNIEnv *env, jobject obj, jboolean state); static void JNICALL javaErrorOccured(JNIEnv *env, jobject obj, jstring error); static void JNICALL javaMyDeviceAttached(JNIEnv *env, jobject obj, jboolean state); private: void qtResponseReady(QByteArray qByteArray); void qtConnectedStateChanged(bool state); void qtErrorOccurred(QString errorMessage);
// mydevice.cpp void MyDevice::connectDevice() { QAndroidJniObject activity = QtAndroid::androidActivity(); QAndroidJniObject context = QtAndroid::androidContext(); if (activity.isValid() && context.isValid()) { QAndroidJniObject::callStaticMethod<void>( "com/mycompany/mydevice/SerialHelper", "connectToDevice", "(Landroid/content/Context;II)V", context.object(), MY_DEVICE_VID, MY_DEVICE_PID ); } } void MyDevice::sendCommand(QByteArray command) { if (command.isEmpty()) return; activeCommand.clear(); activeCommand = command; QAndroidJniObject javaCommand = QAndroidJniObject::fromString(QString(command)); QAndroidJniObject::callStaticMethod<void>( "com/mycompany/mydevice/SerialHelper", "sendCommand", "(Ljava/lang/String;)V", javaCommand.object<jstring>() ); } void MyDevice::javaResponseReady(JNIEnv *env, jobject obj, jbyteArray byteArray) { jbyte *elems = env->GetByteArrayElements(byteArray, 0); jsize len = env->GetArrayLength(byteArray); QByteArray qByteArray(reinterpret_cast<char*>(elems), len); env->ReleaseByteArrayElements(byteArray, elems, 0); MyDevice &instance = MyDevice::getInstance(); instance.qtResponseReady(qByteArray); } void MyDevice::javaConnectedStateChanged(JNIEnv *env, jobject obj, jboolean state) { MyDevice &instance = MyDevice::getInstance(); instance.qtConnectedStateChanged((bool) state); } void MyDevice::javaErrorOccured(JNIEnv *env, jobject obj, jstring error) { const char* utfStr = env->GetStringUTFChars(error, 0); QString qStr = QString::fromUtf8(utfStr); env->ReleaseStringUTFChars(error, utfStr); MyDevice &instance = MyDevice::getInstance(); instance.qtErrorOccurred(qStr); } void MyDevice::javaMyDeviceAttached(JNIEnv *env, jobject obj, jboolean state) { MyDevice &instance = MyDevice::getInstance(); if(state){ instance.connectDevice(); } else { instance.disconnectDevice(); } } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return -1; } JNINativeMethod methods[] { {"javaResponseReady", "([B)V", reinterpret_cast<void*>(MyDevice::javaResponseReady)}, {"javaConnectedStateChanged", "(Z)V", reinterpret_cast<void*>(MyDevice::javaConnectedStateChanged)}, {"javaErrorOccured", "(Ljava/lang/String;)V", reinterpret_cast<void*>(MyDevice::javaErrorOccured)}, {"javaMyDeviceAttached", "(Z)V", reinterpret_cast<void*>(MyDevice::javaMyDeviceAttached)} }; jclass clazz = env->FindClass("com/mycompany/mydevice/SerialHelper"); if (!clazz) { return -1; } if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(JNINativeMethod)) < 0) { return -1; } return JNI_VERSION_1_6; }
// SerialHelper.java package com.mycompany.mydevice; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbManager; import android.util.Log; import com.hoho.android.usbserial.driver.FtdiSerialDriver; import com.hoho.android.usbserial.driver.UsbSerialPort; import com.hoho.android.usbserial.driver.UsbSerialDriver; import com.hoho.android.usbserial.driver.ProbeTable; import com.hoho.android.usbserial.driver.UsbSerialProber; import com.hoho.android.usbserial.util.HexDump; import com.hoho.android.usbserial.util.SerialInputOutputManager; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.List; public class SerialHelper extends org.qtproject.qt5.android.bindings.QtActivity { private static ExecutorService executorService = Executors.newSingleThreadExecutor(); private static final String ACTION_USB_PERMISSION = "com.mycompany.mydevice.USB_PERMISSION"; private static UsbManager usbManager; private static UsbSerialPort serialPort; private static final int WRITE_WAIT_MILLIS = 2000; private static final int READ_WAIT_MILLIS = 2000; public static native void javaResponseReady(byte[] response); public static native void javaConnectedStateChanged(boolean state); public static native void javaErrorOccured(String error); public static native void javaMyDeviceAttached(boolean state); public static void closeDeviceConnection() { executorService.submit(new Runnable() { @Override public void run() { if (serialPort == null) { javaErrorOccured("Serial port is not initialized. Nothing to close."); return; } try { serialPort.close(); serialPort = null; javaConnectedStateChanged(false); return; } catch (IOException e) { javaErrorOccured("CLOSE PORT EXCEPTION"); return; } } }); } public static void connectToDevice(Context context, int vid, int pid) { executorService.submit(new Runnable() { @Override public void run() { // Create custom prober for given VID and PID ProbeTable customTable = new ProbeTable(); customTable.addProduct(vid, pid, FtdiSerialDriver.class); UsbSerialProber prober = new UsbSerialProber(customTable); // Find all available drivers from attached devices. UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); List<UsbSerialDriver> drivers = prober.findAllDrivers(manager); if (drivers.isEmpty()) { javaErrorOccured("No Device found"); return; } // Open a connection to the first available driver. UsbSerialDriver driver = drivers.get(0); UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); if (connection == null) { PendingIntent permissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), 0); manager.requestPermission(driver.getDevice(), permissionIntent); javaErrorOccured("Permission request sent. Awaiting response..."); return; } UsbSerialPort port = driver.getPorts().get(0); try { port.open(connection); try { port.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); } catch (UnsupportedOperationException e){ javaErrorOccured("UNSUPPORTED OPERATION EXCEPTION"); return; } } catch (Exception e) { javaErrorOccured("OPEN PORT EXCEPTION"); return; } usbManager = manager; serialPort = port; javaConnectedStateChanged(true); } }); } public static void sendCommand(String command) { executorService.submit(new Runnable() { @Override public void run() { if (serialPort == null) { javaConnectedStateChanged(false); javaErrorOccured("Serial port is not initialized. Call connectToDevice() first."); return; } try { serialPort.write(command.getBytes(), WRITE_WAIT_MILLIS); } catch (Exception e) { javaErrorOccured("WRITE EXCEPTION"); javaConnectedStateChanged(false); return; } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; try { // Read until command terminator found while (true) { int len = serialPort.read(buffer, READ_WAIT_MILLIS); if (len > 0) { outputStream.write(buffer, 0, len); boolean exit = false; // ADD EXIT CONDITION / response termination pattern (e.g. \r\n) if(exit){ byte[] originalArray = outputStream.toByteArray(); // -2 => terminator (e.g. \r\n) removed before sending to C++ byte[] trimmedArray = Arrays.copyOf(originalArray, originalArray.length - 2); javaResponseReady(trimmedArray); return; } } } } catch (IOException e) { javaErrorOccured("READ EXCEPTION"); javaConnectedStateChanged(false); return; } } }); } private static final BroadcastReceiver usbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (device != null) { javaMyDeviceAttached(true); int vid = device.getVendorId(); int pid = device.getProductId(); connectToDevice(context, vid, pid); } } } } } }; }
// build.gradle buildscript { repositories { google() mavenCentral() maven { url 'https://jitpack.io' } } dependencies { classpath 'com.android.tools.build:gradle:7.2.1' } } repositories { google() mavenCentral() maven { url 'https://jitpack.io' } } apply plugin: 'com.android.application' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation 'androidx.core:core:1.3.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32" implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72" implementation 'com.github.mik3y:usb-serial-for-android:v3.6.0' }
<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.USB_PERMISSION"/> <uses-permission android:name="android.permission.USB_HOST"/> <!-- for automatically opening the app when the device is connected --> <!-- inside <activity> --> <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"/>
<!-- android/res/xml/device_filter.xml --> <?xml version="1.0" encoding="utf-8"?> <resources> <usb-device vendor-id="YOUR_VID" product-id="YOUR_PID" /> <!-- if you have multiple devices --> <usb-device vendor-id="YOUR_VID" product-id="YOUR_PID_SECOND_DEVICE" /> </resources>
-
Hi,
I am porting a Qt application to Android. A major feature of the application relies on using the QSerialPort module, which is not officially supported by Qt Android and it only works on rooted devices which is not a solution for me. Is there a way to port my application without having to make large changes to my code (like switching to BT instead of QSerialPort)?I have seen a patch mentioned before, I think it is this one, https://codereview.qt-project.org/c/qt/qtserialport/+/84338/2 . Is this still the way to make QSerialPort work in Qt 5.15.15 or is there another way?
Kind Regards
Chaz.@chaz AFAIK your device would still have to be rooted to use any SerialPort library and you said:
@chaz said in Using serial port on android:
it only works on rooted devices which is not a solution for me
-
Hi,
I am porting a Qt application to Android. A major feature of the application relies on using the QSerialPort module, which is not officially supported by Qt Android and it only works on rooted devices which is not a solution for me. Is there a way to port my application without having to make large changes to my code (like switching to BT instead of QSerialPort)?I have seen a patch mentioned before, I think it is this one, https://codereview.qt-project.org/c/qt/qtserialport/+/84338/2 . Is this still the way to make QSerialPort work in Qt 5.15.15 or is there another way?
Kind Regards
Chaz.I had the same issue for an FTDI usb device and ended up re-writing the interface with JNI.
For the android part I used this package:
https://github.com/mik3y/usb-serial-for-android/For this package I had to switch to OpenJDK 18, in order to use the required gradle version.
As the JNI part is static, I used the singleton pattern to build an asynchronous interface.
Depending on your current implementation / usage of QSerialPort, you might be able to simply swap out the QSerialPort for a JNI wrapper class on Android.
In total this looks something like this:
- mydevice.h
- mydevice.cpp
- android/src/com/mycompany/mydevice/SerialHelper.java
- changes in AndroidManifest.xml
- changes in build.gradle
In the following some snippets. I tried to reduce the snippets but to still cover the JNI parts.
// mydevice.h // JNI methods public: static void JNICALL javaResponseReady(JNIEnv *env, jobject obj, jbyteArray byteArray); static void JNICALL javaConnectedStateChanged(JNIEnv *env, jobject obj, jboolean state); static void JNICALL javaErrorOccured(JNIEnv *env, jobject obj, jstring error); static void JNICALL javaMyDeviceAttached(JNIEnv *env, jobject obj, jboolean state); private: void qtResponseReady(QByteArray qByteArray); void qtConnectedStateChanged(bool state); void qtErrorOccurred(QString errorMessage);
// mydevice.cpp void MyDevice::connectDevice() { QAndroidJniObject activity = QtAndroid::androidActivity(); QAndroidJniObject context = QtAndroid::androidContext(); if (activity.isValid() && context.isValid()) { QAndroidJniObject::callStaticMethod<void>( "com/mycompany/mydevice/SerialHelper", "connectToDevice", "(Landroid/content/Context;II)V", context.object(), MY_DEVICE_VID, MY_DEVICE_PID ); } } void MyDevice::sendCommand(QByteArray command) { if (command.isEmpty()) return; activeCommand.clear(); activeCommand = command; QAndroidJniObject javaCommand = QAndroidJniObject::fromString(QString(command)); QAndroidJniObject::callStaticMethod<void>( "com/mycompany/mydevice/SerialHelper", "sendCommand", "(Ljava/lang/String;)V", javaCommand.object<jstring>() ); } void MyDevice::javaResponseReady(JNIEnv *env, jobject obj, jbyteArray byteArray) { jbyte *elems = env->GetByteArrayElements(byteArray, 0); jsize len = env->GetArrayLength(byteArray); QByteArray qByteArray(reinterpret_cast<char*>(elems), len); env->ReleaseByteArrayElements(byteArray, elems, 0); MyDevice &instance = MyDevice::getInstance(); instance.qtResponseReady(qByteArray); } void MyDevice::javaConnectedStateChanged(JNIEnv *env, jobject obj, jboolean state) { MyDevice &instance = MyDevice::getInstance(); instance.qtConnectedStateChanged((bool) state); } void MyDevice::javaErrorOccured(JNIEnv *env, jobject obj, jstring error) { const char* utfStr = env->GetStringUTFChars(error, 0); QString qStr = QString::fromUtf8(utfStr); env->ReleaseStringUTFChars(error, utfStr); MyDevice &instance = MyDevice::getInstance(); instance.qtErrorOccurred(qStr); } void MyDevice::javaMyDeviceAttached(JNIEnv *env, jobject obj, jboolean state) { MyDevice &instance = MyDevice::getInstance(); if(state){ instance.connectDevice(); } else { instance.disconnectDevice(); } } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return -1; } JNINativeMethod methods[] { {"javaResponseReady", "([B)V", reinterpret_cast<void*>(MyDevice::javaResponseReady)}, {"javaConnectedStateChanged", "(Z)V", reinterpret_cast<void*>(MyDevice::javaConnectedStateChanged)}, {"javaErrorOccured", "(Ljava/lang/String;)V", reinterpret_cast<void*>(MyDevice::javaErrorOccured)}, {"javaMyDeviceAttached", "(Z)V", reinterpret_cast<void*>(MyDevice::javaMyDeviceAttached)} }; jclass clazz = env->FindClass("com/mycompany/mydevice/SerialHelper"); if (!clazz) { return -1; } if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(JNINativeMethod)) < 0) { return -1; } return JNI_VERSION_1_6; }
// SerialHelper.java package com.mycompany.mydevice; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbManager; import android.util.Log; import com.hoho.android.usbserial.driver.FtdiSerialDriver; import com.hoho.android.usbserial.driver.UsbSerialPort; import com.hoho.android.usbserial.driver.UsbSerialDriver; import com.hoho.android.usbserial.driver.ProbeTable; import com.hoho.android.usbserial.driver.UsbSerialProber; import com.hoho.android.usbserial.util.HexDump; import com.hoho.android.usbserial.util.SerialInputOutputManager; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.List; public class SerialHelper extends org.qtproject.qt5.android.bindings.QtActivity { private static ExecutorService executorService = Executors.newSingleThreadExecutor(); private static final String ACTION_USB_PERMISSION = "com.mycompany.mydevice.USB_PERMISSION"; private static UsbManager usbManager; private static UsbSerialPort serialPort; private static final int WRITE_WAIT_MILLIS = 2000; private static final int READ_WAIT_MILLIS = 2000; public static native void javaResponseReady(byte[] response); public static native void javaConnectedStateChanged(boolean state); public static native void javaErrorOccured(String error); public static native void javaMyDeviceAttached(boolean state); public static void closeDeviceConnection() { executorService.submit(new Runnable() { @Override public void run() { if (serialPort == null) { javaErrorOccured("Serial port is not initialized. Nothing to close."); return; } try { serialPort.close(); serialPort = null; javaConnectedStateChanged(false); return; } catch (IOException e) { javaErrorOccured("CLOSE PORT EXCEPTION"); return; } } }); } public static void connectToDevice(Context context, int vid, int pid) { executorService.submit(new Runnable() { @Override public void run() { // Create custom prober for given VID and PID ProbeTable customTable = new ProbeTable(); customTable.addProduct(vid, pid, FtdiSerialDriver.class); UsbSerialProber prober = new UsbSerialProber(customTable); // Find all available drivers from attached devices. UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); List<UsbSerialDriver> drivers = prober.findAllDrivers(manager); if (drivers.isEmpty()) { javaErrorOccured("No Device found"); return; } // Open a connection to the first available driver. UsbSerialDriver driver = drivers.get(0); UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); if (connection == null) { PendingIntent permissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), 0); manager.requestPermission(driver.getDevice(), permissionIntent); javaErrorOccured("Permission request sent. Awaiting response..."); return; } UsbSerialPort port = driver.getPorts().get(0); try { port.open(connection); try { port.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); } catch (UnsupportedOperationException e){ javaErrorOccured("UNSUPPORTED OPERATION EXCEPTION"); return; } } catch (Exception e) { javaErrorOccured("OPEN PORT EXCEPTION"); return; } usbManager = manager; serialPort = port; javaConnectedStateChanged(true); } }); } public static void sendCommand(String command) { executorService.submit(new Runnable() { @Override public void run() { if (serialPort == null) { javaConnectedStateChanged(false); javaErrorOccured("Serial port is not initialized. Call connectToDevice() first."); return; } try { serialPort.write(command.getBytes(), WRITE_WAIT_MILLIS); } catch (Exception e) { javaErrorOccured("WRITE EXCEPTION"); javaConnectedStateChanged(false); return; } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; try { // Read until command terminator found while (true) { int len = serialPort.read(buffer, READ_WAIT_MILLIS); if (len > 0) { outputStream.write(buffer, 0, len); boolean exit = false; // ADD EXIT CONDITION / response termination pattern (e.g. \r\n) if(exit){ byte[] originalArray = outputStream.toByteArray(); // -2 => terminator (e.g. \r\n) removed before sending to C++ byte[] trimmedArray = Arrays.copyOf(originalArray, originalArray.length - 2); javaResponseReady(trimmedArray); return; } } } } catch (IOException e) { javaErrorOccured("READ EXCEPTION"); javaConnectedStateChanged(false); return; } } }); } private static final BroadcastReceiver usbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (device != null) { javaMyDeviceAttached(true); int vid = device.getVendorId(); int pid = device.getProductId(); connectToDevice(context, vid, pid); } } } } } }; }
// build.gradle buildscript { repositories { google() mavenCentral() maven { url 'https://jitpack.io' } } dependencies { classpath 'com.android.tools.build:gradle:7.2.1' } } repositories { google() mavenCentral() maven { url 'https://jitpack.io' } } apply plugin: 'com.android.application' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation 'androidx.core:core:1.3.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32" implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72" implementation 'com.github.mik3y:usb-serial-for-android:v3.6.0' }
<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.USB_PERMISSION"/> <uses-permission android:name="android.permission.USB_HOST"/> <!-- for automatically opening the app when the device is connected --> <!-- inside <activity> --> <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"/>
<!-- android/res/xml/device_filter.xml --> <?xml version="1.0" encoding="utf-8"?> <resources> <usb-device vendor-id="YOUR_VID" product-id="YOUR_PID" /> <!-- if you have multiple devices --> <usb-device vendor-id="YOUR_VID" product-id="YOUR_PID_SECOND_DEVICE" /> </resources>
-