A Node.js library for interfacing with WCH CH347 USB devices. Supports GPIO control and SPI flash programming.
Note: This is an early release (v0.0.1). The API may change in future versions.
- GPIO Control: 8 GPIO channels with input/output support
- SPI Flash Programming: Read, write, erase, and verify SPI flash chips
- Supports common chips: W25Qxx, GD25Qxx, MX25Lxx, MT25Qxx, etc.
- JEDEC ID detection
- Sector (4KB), block (32KB/64KB), and chip erase
- UART Path Discovery: Get the serial port path for use with external libraries
- Cross-Platform: Works on Linux, macOS, and Windows
npm install node-ch347Linux:
# Install libusb
sudo apt-get install libusb-1.0-0-dev
# Add udev rules for non-root access
sudo tee /etc/udev/rules.d/99-ch347.rules << EOF
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55db", MODE="0666"
EOF
sudo udevadm control --reload-rules
sudo udevadm triggermacOS:
# Install libusb via Homebrew
brew install libusbWindows:
Windows requires a USB driver that exposes the CH347. Choose one of these options:
Option 1: UsbDk (Recommended) - Coexists with WCH vendor driver
- Download and install UsbDk from: https://github.com/daynix/UsbDk/releases
- Both node-ch347 and WCH's official tools will work
Option 2: WinUSB via Zadig - Replaces vendor driver
- Download Zadig from: https://zadig.akeo.ie/
- Run Zadig as Administrator
- Options → List All Devices
- Select your CH347 device (VID: 1A86, PID: 55DB)
- Select "WinUSB" as the target driver
- Click "Replace Driver"
Note: After installing WinUSB, WCH's official tools (CH347Demo, etc.) will no longer work. Use UsbDk if you need both.
Option 3: WCH DLL - Use WCH's proprietary CH347DLL.dll
- Download CH347EVT from: https://www.wch.cn/downloads/CH341PAR_ZIP.html
- Extract
CH347DLL.dll(orCH347DLLA64.dllfor 64-bit) to your application directory or system PATH - Install FFI dependencies:
npm install ffi-napi ref-napi - Use the
CH347WCHclass instead ofCH347Device
Note: The WCH DLL is NOT included in this package due to licensing concerns. You must obtain it from WCH directly.
By default, the library uses "auto" mode which tries UsbDk first, then falls back to WinUSB.
import { setWindowsBackend } from 'node-ch347';
// Use UsbDk backend (recommended, coexists with vendor driver)
setWindowsBackend('usbdk');
// Use native WinUSB (requires Zadig driver replacement)
setWindowsBackend('winusb');
// Use WCH's CH347DLL.dll (requires DLL in PATH)
setWindowsBackend('wch');
// Auto-detect (try UsbDk first, fall back to WinUSB)
setWindowsBackend('auto');Or set via environment variable:
set CH347_USB_BACKEND=usbdk
set CH347_USB_BACKEND=winusb
set CH347_USB_BACKEND=wchThe WCH DLL backend works with the same CH347Device class - just set the backend:
import CH347Device, { setWindowsBackend, SPISpeed } from 'node-ch347';
// Option 1: Set globally before opening any device
setWindowsBackend('wch');
// Option 2: Set per-device via options
const device = new CH347Device({
backend: 'wch',
spi: { speed: SPISpeed.CLK_15M }
});
await device.open();
// Same API as other backends
const flashInfo = await device.flashReadId();
console.log(`Flash: ${flashInfo.name}`);
await device.gpioWrite(3, true);
const states = await device.gpioReadAll();
device.close();You can also use the low-level CH347WCH class directly for advanced use cases.
import CH347Device, { SPISpeed } from 'node-ch347';
async function main() {
// Create device instance
const ch347 = new CH347Device({
spi: { speed: SPISpeed.CLK_15M }
});
// Open connection
await ch347.open();
// GPIO: Set pin 3 high
await ch347.gpioWrite(3, true);
// SPI Flash: Read chip ID
const flashInfo = await ch347.flashReadId();
console.log(`Flash: ${flashInfo.name}, ${flashInfo.size / 1024 / 1024} MB`);
// SPI Flash: Read data
const data = await ch347.flashRead(0, 4096);
// Close connection
ch347.close();
}Main class that provides unified access to all functionality.
const device = new CH347Device(options?: {
spi?: Partial<SPIConfig>;
});CH347Device.listDevices(): List all connected CH347 devices
Connection:
open(deviceIndex?: number): Open device connectionclose(): Close connectionisConnected(): Check if connectedgetUARTPath(): Get the tty path for this device (e.g.,/dev/ttyACM0)
GPIO:
gpioReadAll(): Read all GPIO pin statesgpioRead(pin): Read single GPIO pingpioWrite(pin, value): Set GPIO outputgpioPulse(pin, duration, activeHigh): Pulse GPIO pin
SPI Flash:
spiInit(config?): Initialize SPI interfaceflashReadId(): Read JEDEC IDflashRead(address, length, onProgress?): Read dataflashWrite(address, data, onProgress?): Write dataflashEraseSector(address): Erase 4KB sectorflashEraseChip(onProgress?): Erase entire chipflashProgram(address, data, options?): Erase + write + verifyflashProgramFile(filePath, address?, options?): Program flash from binary file (if address omitted, file size must match flash size)flashReadToFile(filePath, address?, length?, onProgress?): Read flash to binary file
interface SPIConfig {
speed: SPISpeed; // Clock speed (default: CLK_15M)
mode: SPIMode; // SPI mode 0-3 (default: MODE_0)
chipSelect: 0 | 1; // CS line (default: 0)
bitOrder: 'MSB' | 'LSB'; // Bit order (default: 'MSB')
}
enum SPISpeed {
CLK_60M = 0,
CLK_30M = 1,
CLK_15M = 2,
CLK_7_5M = 3,
CLK_3_75M = 4,
CLK_1_875M = 5,
CLK_937_5K = 6,
CLK_468_75K = 7,
}import CH347Device from 'node-ch347';
const ch347 = new CH347Device();
await ch347.open();
// Read all GPIO states
const states = await ch347.gpioReadAll();
states.forEach(s => console.log(`GPIO${s.pin}: ${s.value}`));
// Set output on pin 3
await ch347.gpioWrite(3, true);
// Pulse pin 0 (for button press simulation)
await ch347.gpioPulse(0, 100);
ch347.close();import CH347Device, { SPISpeed } from 'node-ch347';
const ch347 = new CH347Device({ spi: { speed: SPISpeed.CLK_30M } });
await ch347.open();
// Detect flash
const info = await ch347.flashReadId();
console.log(`Detected: ${info.name}`);
// Backup flash to file
await ch347.flashReadToFile('backup.bin', 0, info.size, (p) => {
console.log(`Reading: ${p.percentage}%`);
});
// Program flash from file (erase + write + verify)
await ch347.flashProgramFile('new_firmware.bin', 0, {
erase: true,
verify: true,
onProgress: (p) => console.log(`${p.operation}: ${p.percentage}%`)
});
ch347.close();The library provides UART path discovery on Linux and macOS. Use an external serial library like serialport for actual communication:
Note: Windows UART path discovery is not yet implemented. On Windows, manually specify the COM port (e.g.,
COM3).
import CH347Device from 'node-ch347';
import { SerialPort } from 'serialport'; // npm install serialport
const ch347 = new CH347Device();
await ch347.open();
// Get UART path for this device
const uartPath = ch347.getUARTPath();
console.log('UART path:', uartPath); // e.g., '/dev/ttyACM0'
// Use serialport library for UART communication
if (uartPath) {
const port = new SerialPort({ path: uartPath, baudRate: 115200 });
port.write('AT\r\n');
// ... handle data with port.on('data', ...)
}
ch347.close();The CH347 is a USB 2.0 high-speed (480 Mbps) bus converter chip by WCH (Nanjing Qinheng Microelectronics).
Supported mode:
- Mode 1 (PID 0x55DB): UART + SPI + I2C + GPIO
Pin assignments for GPIO (Mode 1):
- GPIO0: CTS0/SCK/TCK
- GPIO1: RTS0/MISO/TDO
- GPIO2: DSR0/SCS0/TMS
- GPIO3: SCL
- GPIO4: ACT
- GPIO5: DTR0/TNOW0/SCS1/TRST
- GPIO6: CTS1
- GPIO7: RTS1
This project uses git hooks for pre-push validation. To enable them:
git config core.hooksPath .githooksThe pre-push hook runs:
- Build (
npm run build) - Tests (
npm test) - Type checking (
npx tsc --noEmit)
If you have Claude Code installed, the pre-push hook will automatically run /check-style to analyze code for API consistency and style issues. This is optional and non-blocking.
This project was developed with AI-assisted code generation. Not all functions have been fully tested. Use at your own risk.
This project is released into the public domain under The Unlicense.