npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-usb-serial

v0.0.2

Published

Android-only Expo module for USB serial communication over USB OTG. Wraps the rock-solid usb-serial-for-android (mik3y) Java library with support for FTDI, CH340, CP210x, and PL2303 chips.

Readme

expo-usb-serial

Android-only Expo native module for USB serial communication over USB OTG.
Wraps mik3y's usb-serial-for-android Java library.

Why this exists: Other community packages are unmaintained or require manual configuration of build dependencies. This module wraps the underlying Java library using the modern Expo Modules API.


[!WARNING] Expo Go Compatibility Because this package utilizes custom native code (and compiles mik3y's usb-serial-for-android Java library under the hood) to talk to physical USB OTG serial devices, it will not work in the standard pre-compiled Expo Go application.

To test and run this library, you must compile it into an Android Development Build (expo-dev-client) by running npx expo run:android or running it in a vanilla React Native project.

Supported chips

| Silicon | Examples | | ----------------------- | ------------------------------------------------------------- | | FTDI | FT232R, FT2232H, FT231X | | Silicon Labs CP210x | CP2102, CP2104, CP2109 | | WCH CH340/CH341 | CH340G, CH341A | | Prolific PL2303 | PL2303HX, PL2303TA | | CDC-ACM | Arduino Uno/Mega/Leonardo, Teensy, STM32, ESP32-S3 native USB |


Installation

npx expo install expo-usb-serial

Add the Config Plugin

In your app.json / app.config.js:

{
  "plugins": [["expo-usb-serial"]]
}

Then run a new build:

npx expo prebuild --platform android
# or
eas build --platform android

The config plugin configures the Android USB host feature, adds the necessary intent-filters for hardware detection, and generates the device filters for the supported silicon chips.

Custom device filter

To restrict which devices trigger your app, or to add exotic chips, define them in the config plugin.

[!IMPORTANT] Decimal Format Required: Standard JSON does not support hexadecimal literal notation (like 0x0403) or inline comments. Therefore, Vendor IDs and Product IDs must be specified as base-10 (decimal) integers in your app.json config.

  • Converting Hex to Decimal: Most hardware datasheets list IDs in hex format (e.g. 0x0403 for FTDI, 0x10C4 for CP210x). You must convert these to decimal:
    • 0x0403 (hex) ➔ 1027 (decimal)
    • 0x10C4 (hex) ➔ 4292 (decimal)
    • 0x1A86 (hex) ➔ 6790 (decimal)
    • 0x067B (hex) ➔ 1659 (decimal)
  • You can easily perform this conversion in JavaScript/Node using parseInt("0403", 16), by looking up the vendor ID in the VendorIds constant, or by using a developer calculator.
{
  "plugins": [
    [
      "expo-usb-serial",
      {
        "devices": [
          { "vendorId": 1027, "label": "FTDI" },
          { "vendorId": 4292, "productId": 60000, "label": "My CP2102 dongle" }
        ]
      }
    ]
  ]
}

To disable auto-launch on device plug-in:

["expo-usb-serial", { "autoLaunchOnConnect": false }]

API

import {
  listDevices,
  requestPermission,
  connect,
  isConnected,
  listConnected,
  isAvailable,
  VendorIds,
  UsbSerialConnection,
  type UsbSerialDevice,
  type ConnectOptions,
  type UsbSerialUnavailableError,
} from "expo-usb-serial";

listDevices({ customDevices }?): Promise<UsbSerialDevice[]>

Returns all attached USB devices that have a supported driver. You can optionally pass a list of custom devices to map proprietary or unrecognized USB Vendor/Product IDs to standard chip drivers:

// Standard discovery
const devices = await listDevices();

// Discovery with custom vendor chip mappings
const devices = await listDevices({
  customDevices: [
    { vendorId: 0x1234, productId: 0x5678, driverName: "FtdiSerial" }
  ]
});
// [{ deviceId: 3, vendorId: 4660, productId: 22136, driverName: "FtdiSerial", portCount: 1 }]

requestPermission({ deviceId }): Promise<boolean>

Shows the Android system dialog asking the user to grant access to the USB device. Returns true if granted, or false if denied.

Note: The permission request will automatically time out and reject the promise after 30 seconds if the user ignores or backgrounds the dialog, cleanly releasing background receiver resources.

const granted = await requestPermission({ deviceId: devices[0].deviceId });

connect({ deviceId, options }?): Promise<UsbSerialConnection>

Opens a serial connection and returns a JSI-backed UsbSerialConnection instance representing the active port. Starts the background read loop automatically.

const connection = await connect({
  deviceId,
  options: {
    baudRate: 115200, // default: 9600
    dataBits: 8, // default: 8
    stopBits: 1, // default: 1
    parity: "none", // "none" | "odd" | "even" | "mark" | "space"
    portIndex: 0, // for multi-port FTDI devices
  }
});

UsbSerialConnection Class

A JSI-backed native connection instance. Exposes synchronous direct methods directly on the JavaScript thread:

  • connection.write(data: Uint8Array, timeoutMs?: number): number (JSI Sync) Sends raw bytes directly to the hardware using direct JSI memory mapping. Runs synchronously on the JavaScript execution thread. Best for ultra-low latency high-priority write cycles where blocking for <1ms is acceptable.

    const data = new Uint8Array([0x01, 0xab, 0x03]);
    const written = connection.write(data);
  • connection.writeAsync(data: Uint8Array, timeoutMs?: number): Promise<number> (JSI Async) Sends raw bytes asynchronously by delegating the blocking OS I/O operation to a native background thread pool. Best for large payloads or standard writes to fully safeguard the JS UI thread from blocking freezes.

    const data = new Uint8Array([0x01, 0xab, 0x03]);
    const written = await connection.writeAsync(data);
  • connection.disconnect(): void Closes the port and terminates the background read loop synchronously.

    connection.disconnect();
  • connection.setDtr(enabled: boolean): void Toggles the DTR pin signal state synchronously (commonly used to reset microcontrollers like Arduino).

    connection.setDtr(false);
  • connection.setRts(enabled: boolean): void Toggles the RTS pin signal state synchronously.

    connection.setRts(true);
  • connection.isConnected(): boolean Synchronous state check to see if the port is currently open.

    const open = connection.isConnected();
  • connection.addDataListener(listener: (data: Uint8Array) => void): { remove(): void } Subscribe to incoming data from this connection instance. Callback fires for every read chunk and receives raw bytes directly as a JS Uint8Array via JSI:

    const sub = connection.addDataListener((data) => {
      const text = Array.from(data).map(b => String.fromCharCode(b)).join("");
      console.log("Received data:", text);
    });
      
    // When done:
    sub.remove();
  • connection.addErrorListener(listener: (error: { code: string; message: string }) => void): { remove(): void } Subscribe to connection-specific error events. The most important error code is "ERR_USB_DISCONNECTED" — emitted when the USB cable is physically unplugged.

    const sub = connection.addErrorListener((e) => {
      console.warn(`[${e.code}] ${e.message}`);
      if (e.code === "ERR_USB_DISCONNECTED") {
        console.log("Device disconnected!");
      }
    });
      
    // When done:
    sub.remove();

isConnected({ deviceId }): boolean

Synchronous module-level check — returns true if the device ID currently has any active open connections.

const active = isConnected({ deviceId });

listConnected(): number[]

Synchronous check — returns a list of device IDs for all USB serial devices currently connected (with active open connections). No I/O is performed.

const activeDeviceIds = listConnected();
console.log("Active connection IDs:", activeDeviceIds);

isAvailable: boolean

true on Android with a native build. false on iOS, web, or Expo Go.

VendorIds

Convenience constants for common vendor IDs (decimal):

VendorIds.FTDI;     // 1027   (0x0403)
VendorIds.SILABS;   // 4292   (0x10C4)
VendorIds.WCH;      // 6790   (0x1A86)
VendorIds.PROLIFIC; // 1659   (0x067B)
VendorIds.ARDUINO;  // 9025   (0x2341)

Native Error Codes Reference

When the native layer rejects a promise or emits an onUsbSerialError event, it includes a short code string. Here is a reference of all possible error codes:

| Error Code | Occurs When | |------------|-------------| | "ERR_USB_DISCONNECTED" | Emitted dynamically when the USB cable is physically unplugged. | | "ERR_USB_READ" | The background read thread fails to read from the hardware. | | "ERR_USB_WRITE" | A write operation to the hardware fails or times out. | | "ERR_USB_PERMISSION_TIMEOUT" | The user ignores the Android system USB permission dialog for more than 30 seconds. | | "ERR_USB_PERMISSION" | An OS exception occurs while requesting permissions. | | "ERR_USB_NOT_FOUND" | The specified deviceId is not currently attached. | | "ERR_USB_ALREADY_OPEN" | Attempting to connect() to a deviceId that already has an open connection. | | "ERR_USB_NO_DRIVER" | No standard or custom driver is matching the USB chipset. | | "ERR_USB_INVALID_PORT" | The portIndex exceeds the number of available physical ports on the device. | | "ERR_USB_OPEN" | The OS fails to open the device (most commonly due to missing USB permission). | | "ERR_USB_CONNECT" | The serial port fails to configure or open at the hardware level. | | "ERR_USB_DISCONNECT" | Failed to cleanly close the connection. | | "ERR_USB_LINE_CONTROL" | Failed to toggle the DTR or RTS hardware control line state. | | "ERR_USB_LIST" | An exception occurs while probing physical USB interfaces. |


Full Example

import React, { useEffect, useRef, useState } from "react";
import { Button, FlatList, Text, View } from "react-native";
import {
  listDevices,
  requestPermission,
  connect,
  isAvailable,
  UsbSerialConnection,
  type UsbSerialDevice,
} from "expo-usb-serial";

export default function App() {
  const [devices, setDevices] = useState<UsbSerialDevice[]>([]);
  const [activeConnection, setActiveConnection] = useState<UsbSerialConnection | null>(null);
  const [log, setLog] = useState<string[]>([]);
  const subs = useRef<Array<{ remove(): void }>>([]);

  const appendLog = (msg: string) =>
    setLog((prev) => [`${new Date().toISOString().slice(11, 19)} ${msg}`, ...prev.slice(0, 99)]);

  const scan = async () => {
    if (!isAvailable) return appendLog("USB serial not available on this platform");
    const found = await listDevices();
    setDevices(found);
    appendLog(`Found ${found.length} device(s)`);
  };

  const openDevice = async (device: UsbSerialDevice) => {
    const granted = await requestPermission({ deviceId: device.deviceId });
    if (!granted) return appendLog("Permission denied");

    const connection = await connect({ deviceId: device.deviceId, options: { baudRate: 115200 } });
    setActiveConnection(connection);
    appendLog(`Connected to ${device.productName ?? `device ${device.deviceId}`}`);

    subs.current.push(
      connection.addDataListener((data) => {
        const text = Array.from(data).map(b => String.fromCharCode(b)).join("").trim();
        if (text) appendLog(`← ${text}`);
      }),
      connection.addErrorListener((e) => {
        appendLog(`⚠ ${e.code}: ${e.message}`);
        if (e.code === "ERR_USB_DISCONNECTED") setActiveConnection(null);
      })
    );
  };

  const closeDevice = async () => {
    if (activeConnection !== null) {
      subs.current.forEach((s) => s.remove());
      subs.current = [];
      activeConnection.disconnect();
      setActiveConnection(null);
      appendLog("Disconnected");
    }
  };

  const sendPing = async () => {
    if (activeConnection === null) return;
    const data = new Uint8Array([112, 105, 110, 103, 10]); // "ping\n"
    const n = activeConnection.write(data);
    appendLog(`→ sent ${n} bytes`);
  };

  useEffect(() => () => { subs.current.forEach((s) => s.remove()); }, []);

  return (
    <View style={{ flex: 1, padding: 24, gap: 12 }}>
      <Button title="Scan for devices" onPress={scan} />
      {devices.map((d) => (
        <Button
          key={d.deviceId}
          title={`Connect: ${d.productName ?? d.driverName} (${d.vendorId}:${d.productId})`}
          onPress={() => openDevice(d)}
          disabled={activeConnection !== null}
        />
      ))}
      {activeConnection !== null && (
        <>
          <Button title="Send ping" onPress={sendPing} />
          <Button title="Disconnect" onPress={closeDevice} color="red" />
        </>
      )}
      <FlatList
        data={log}
        keyExtractor={(_, i) => String(i)}
        renderItem={({ item }) => (
          <Text style={{ fontFamily: "monospace", fontSize: 12 }}>{item}</Text>
        )}
      />
    </View>
  );
}

Platform Support

| Platform | Status | | -------- | ----------------------------------------------------------------------------------------------- | | Android | ✅ Full support | | iOS | ❌ Not supported (Apple restricts generic USB serial; use BLE or Lightning accessories instead) | | Web | ❌ Not supported |


JitPack Dependency (Zero-Config)

The usb-serial-for-android library is hosted on JitPack.

No manual setup is required! The config plugin handles this during prebuild.


Hardware Testing & Troubleshooting Tips

When working with low-level USB OTG serial devices on Android, keep these hardware and OS constraints in mind:

Bus Power Limits

Many development boards and 3D printer controllers (like the CH340 chip on the Snapmaker U1) pull power directly from the USB host line to stay discoverable. If your phone aggressively restricts power output, plugging into a turned-off printer can cause connection drops. Always power on the printer or peripheral before connecting the USB cable to your phone.

System Intent Prompts

The moment you connect your serial device via a USB OTG cable, the Android OS broadcasts a UsbManager.ACTION_USB_DEVICE_ATTACHED intent. Because this module's Config Plugin automatically wires the intent-filter onto your main activity, Android will instantly display a system dialog asking: "Allow [Your App] to access [Device Name]?" and can automatically launch your application.


Contributing

Issues and PRs welcome at github.com/bc-bane/expo-usb-serial.

License

MIT