Android Serial Communication via JNI: Native UART Access and Java Integration
Native layer (UartPort.cpp)
#include <jni.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <android/log.h>
#include <string.h>
#define LOG_TAG "uart_port"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
static speed_t map_baud(jint b) {
switch (b) {
case 0: return B0; case 50: return B50; case 75: return B75; case 110: return B110;
case 134: return B134; case 150: return B150; case 200: return B200; case 300: return B300;
case 600: return B600; case 1200: return B1200; case 1800: return B1800; case 2400: return B2400;
case 4800: return B4800; case 9600: return B9600; case 19200: return B19200; case 38400: return B38400;
case 57600: return B57600; case 115200: return B115200; case 230400: return B230400; case 460800: return B460800;
case 500000: return B500000; case 576000: return B576000; case 921600: return B921600; case 1000000: return B1000000;
case 1152000: return B1152000; case 1500000: return B1500000; case 2000000: return B2000000; case 2500000: return B2500000;
case 3000000: return B3000000; case 3500000: return B3500000; case 4000000: return B4000000;
default: return static_cast<speed_t>(-1);
}
}
static jobject jni_open(JNIEnv* env, jobject thiz, jstring jpath, jint baud) {
speed_t spd = map_baud(baud);
if (spd == static_cast<speed_t>(-1)) {
LOGE("Unsupported baudrate: %d", baud);
return nullptr;
}
const char* path = env->GetStringUTFChars(jpath, nullptr);
if (!path) return nullptr;
LOGI("Opening %s", path);
int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
env->ReleaseStringUTFChars(jpath, path);
if (fd < 0) {
LOGE("open() failed");
return nullptr;
}
// Optional: switch to blocking after open
int flags = fcntl(fd, F_GETFL);
if (flags != -1) {
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
}
struct termios tio;
if (tcgetattr(fd, &tio) != 0) {
LOGE("tcgetattr() failed");
close(fd);
return nullptr;
}
cfmakeraw(&tio);
cfsetispeed(&tio, spd);
cfsetospeed(&tio, spd);
tio.c_cflag |= (CLOCAL | CREAD);
// 8N1
tio.c_cflag &= ~PARENB;
tio.c_cflag &= ~CSTOPB;
tio.c_cflag &= ~CSIZE;
tio.c_cflag |= CS8;
// read returns as soon as at least 1 byte is available
tio.c_cc[VMIN] = 1;
tio.c_cc[VTIME] = 0;
if (tcsetattr(fd, TCSANOW, &tio) != 0) {
LOGE("tcsetattr() failed");
close(fd);
return nullptr;
}
jclass fdCls = env->FindClass("java/io/FileDescriptor");
if (!fdCls) {
close(fd);
return nullptr;
}
jmethodID ctor = env->GetMethodID(fdCls, "<init>", "()V");
jfieldID desc = env->GetFieldID(fdCls, "descriptor", "I");
jobject jfd = env->NewObject(fdCls, ctor);
env->SetIntField(jfd, desc, (jint)fd);
return jfd;
}
static jint jni_close(JNIEnv* env, jobject thiz) {
jclass cls = env->GetObjectClass(thiz);
jfieldID fid = env->GetFieldID(cls, "mFd", "Ljava/io/FileDescriptor;");
jobject jfd = env->GetObjectField(thiz, fid);
jclass fdCls = env->FindClass("java/io/FileDescriptor");
jfieldID desc = env->GetFieldID(fdCls, "descriptor", "I");
jint fd = env->GetIntField(jfd, desc);
if (fd >= 0) {
LOGI("close(fd=%d)", fd);
close(fd);
env->SetIntField(jfd, desc, -1);
return 0;
}
return -1;
}
static JNINativeMethod kMethods[] = {
{"openNative", "(Ljava/lang/String;I)Ljava/io/FileDescriptor;", (void*)jni_open},
{"closeNative", "()I", (void*)jni_close},
};
static int register_natives(JNIEnv* env) {
// Must match the Java class full name
const char* kClassName = "com/example/uart/UartPort";
jclass clazz = env->FindClass(kClassName);
if (!clazz) return JNI_FALSE;
if (env->RegisterNatives(clazz, kMethods, sizeof(kMethods)/sizeof(kMethods[0])) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv* env = nullptr;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
if (!register_natives(env)) return -1;
return JNI_VERSION_1_6;
}
Notes:
- Update the kClassName in register_natives to match your Java package/class.
- The code configures 8N1, raw mode, and minimal blocking semantics (VMIN=1, VTIME=0).
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
TARGET_PLATFORM := android-3
LOCAL_MODULE := uart_port
LOCAL_SRC_FILES := UartPort.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
Change LOCAL_MODULE to alter the produced .so name.
Java wrapper (UartPort.java)
package com.example.uart;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class UartPort {
// Do not rename mFd: native code depends on this field
private FileDescriptor mFd;
private FileInputStream in;
private FileOutputStream out;
public UartPort(File dev, int baud) throws IOException {
mFd = openNative(dev.getAbsolutePath(), baud);
if (mFd == null) throw new IOException("open failed");
in = new FileInputStream(mFd);
out = new FileOutputStream(mFd);
}
public InputStream getInputStream() { return in; }
public OutputStream getOutputStream() { return out; }
private native FileDescriptor openNative(String path, int baudrate);
public native int closeNative();
static {
System.loadLibrary("uart_port");
}
}
Utility class with background reader (UartManager.java)
package com.example.uart;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class UartManager {
public interface OnReceiveListener {
void onBytes(byte[] data, int len);
}
private static final String TAG = "UartManager";
private static UartManager INSTANCE;
private UartPort port;
private InputStream is;
private OutputStream os;
private ReaderLoop reader;
private volatile boolean stop;
private String devicePath = "/dev/ttyMT1";
private int deviceBaud = 115200;
private OnReceiveListener listener;
public static synchronized UartManager get() {
if (INSTANCE == null) {
INSTANCE = new UartManager();
INSTANCE.init();
}
return INSTANCE;
}
public void setOnReceiveListener(OnReceiveListener l) {
this.listener = l;
}
private void init() {
try {
port = new UartPort(new File(devicePath), deviceBaud);
is = port.getInputStream();
os = port.getOutputStream();
stop = false;
reader = new ReaderLoop();
reader.start();
} catch (Exception e) {
Log.e(TAG, "init error", e);
}
}
public boolean writeLine(String ascii) {
// Append CRLF commonly required by many modules; adjust as needed
byte[] payload = (ascii + "\r\n").getBytes();
return writeRaw(payload);
}
public boolean writeRaw(byte[] bytes) {
if (os == null) return false;
try {
os.write(bytes);
os.flush();
return true;
} catch (IOException e) {
Log.e(TAG, "write error", e);
return false;
}
}
private class ReaderLoop extends Thread {
@Override public void run() {
final byte[] buf = new byte[512];
while (!stop && !isInterrupted()) {
try {
if (is == null) break;
int n = is.read(buf);
if (n > 0 && listener != null) {
listener.onBytes(buf, n);
}
// small pause to reduce CPU load
try { Thread.sleep(10); } catch (InterruptedException ignored) { }
} catch (IOException e) {
Log.e(TAG, "read error", e);
break;
}
}
}
}
public void shutdown() {
stop = true;
if (reader != null) reader.interrupt();
try {
if (port != null) port.closeNative();
} catch (Throwable t) {
Log.w(TAG, "close warning", t);
}
}
}
Build and integrate
- Install and configure the Android NDK.
- Create a jni dierctory at the module root and place Android.mk and UartPort.cpp inside it.
- Run ndk-build from the module directory to produce the shared library (default output under libs/armeabi for legacy setups, or app/build/intermediates if integrated with Gradle).
- Ensure the Java package/class in native regitsration matches the actual UartPort class (com/example/uart/UartPort) or update kClassName accordingly.
- Place UartPort.java and UartManager.java under the matching package path.
- Initialize UartManager where needed and implement OnReceiveListener to receive incoming bytes.
Key points:
- Reading/writing over UART is equivalent to file I/O against a device node in /dev.
- JNI exposes a FileDescriptor to Java, allowing InputStream/OutputStream to operate directly on the serial device.