Using serial port on android
-
wrote on 28 Nov 2023, 14:22 last edited by 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. -
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.wrote on 29 Nov 2023, 10:51 last edited by lemonsI 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.wrote on 29 Nov 2023, 10:51 last edited by lemonsI 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>
-
1/3