Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Android Serial Communication via JNI: Native UART Access and Java Integration

Tech 1

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.