A comprehensive Node.js library for communicating with OBD2 (On-Board Diagnostics) systems in vehicles. Supports serial (USB), Bluetooth (BLE), and WiFi (TCP) connections to ELM327 adapters.
- Universal OBD2 Support: Compatible with all OBD2-compliant vehicles (1996+)
- Multiple Connection Types: Serial (USB/RS232), Bluetooth (Web BLE), and WiFi (TCP)
- Comprehensive Parameter Set: 23+ predefined OBD2 parameters with proper decoders
- Real-time Monitoring: Event-driven data streaming capabilities
- Adapter Management: Automatic adapter initialization and configuration
- Cross-platform: Works on Windows, macOS, and Linux
- TypeScript Support: Full TypeScript definitions included
- Well Tested: Comprehensive test suite with 23+ tests passing
- Clone Compatibility: Supports old ELM327 v1.5/v2.1 clones with compatibility modes
- CAN Bus Monitoring: Capture raw CAN traffic with AT MA/AT MP commands
- Flow Control: Full support for CAN flow control (AT FC SH/SD/SM/CFC)
- Freeze Frame: Read snapshot data at moment of fault (Mode 02)
- Dynamic PID Scanning: Automatically discover supported PIDs (00, 20, 40, 60...)
- Exponential Backoff: Smart reconnection with increasing delays
- NRC Parsing: Human-readable Negative Response Code messages
- BLE Smart Discovery: Multiple UUID patterns for clone adapter support
- File Logging: Persistent logging to file with RAW, PRETTY, or JSON formats
npm install elm327or
yarn add elm327const { OBD2Client, listSerialPorts } = require('elm327');
async function main() {
// List available serial ports
const ports = await listSerialPorts();
console.log('Available ports:', ports);
// Create client
const client = new OBD2Client({
type: 'serial',
port: '/dev/ttyUSB0', // or 'COM3' on Windows
baudRate: 38400,
});
// Connect and initialize
await client.connect();
// Read some basic parameters
const rpm = await client.getRPM();
const speed = await client.getSpeed();
const temp = await client.getCoolantTemperature();
console.log(`RPM: ${rpm}`);
console.log(`Speed: ${speed} km/h`);
console.log(`Coolant: ${temp}°C`);
await client.disconnect();
}
main().catch(console.error);import { OBD2Client, ConnectionConfig, OBD2Response } from 'elm327';
const config: ConnectionConfig = {
type: 'serial',
port: '/dev/ttyUSB0',
baudRate: 38400,
timeout: 5000,
};
const client = new OBD2Client(config);
client.on('connected', () => console.log('Connected!'));
client.on('response', (response: OBD2Response) => {
console.log(`${response.command}: ${response.value} ${response.unit}`);
});
await client.connect();
const engineLoad = await client.getEngineLoad();const { OBD2Client } = require('elm327');
const client = new OBD2Client({
type: 'wifi',
host: '192.168.0.10', // Default ELM327 WiFi adapter IP
port: 35000, // Default ELM327 WiFi port
});
await client.connect();
const rpm = await client.getRPM();
console.log(`RPM: ${rpm}`);
await client.disconnect();import { OBD2Client, ConnectionConfig } from 'elm327';
const config: ConnectionConfig = {
type: 'bluetooth',
// Smart discovery will try multiple known UUIDs for clone support
flowControl: {
enabled: true,
header: '0x7E0', // CAN ID for flow control
},
};
const client = new OBD2Client(config);
await client.connect();const client = new OBD2Client(config);| Option | Type | Required | Default | Description |
|---|---|---|---|---|
type |
'serial' | 'bluetooth' | 'wifi' |
Yes | - | Connection type |
port |
string |
For serial | - | Serial port path (e.g., /dev/ttyUSB0, COM3) |
address |
string |
For bluetooth | - | Bluetooth device address (optional for Web Bluetooth API) |
host |
string |
For wifi | 192.168.0.10 | WiFi adapter IP address |
port |
string | number |
For wifi | 35000 | WiFi adapter port |
baudRate |
number |
No | 38400 | Serial baud rate |
timeout |
number |
No | 5000 | Command timeout in milliseconds |
lineEnding |
string |
No | '\r' | Line ending character |
maxLines |
number |
No | 0 | Max log file lines (0 = unlimited). Oldest lines trimmed automatically |
cloneCompatibility |
'auto' | 'strict' | 'lenient' | 'minimal' |
No | 'auto' | Clone compatibility mode |
flowControl |
object |
No | - | Flow control configuration |
-
'auto': Detect and adjust automatically (default) -
'strict': Full feature set, may fail on old clones -
'lenient': Skip unsupported commands, longer delays -
'minimal': Only essential commands (ATZ, ATE0, ATSP0)
flowControl: {
enabled: true,
header: '0x7E0', // CAN ID for flow control
data: '0x30', // Flow control data byte
mode: 1, // Flow control mode (optional)
}await client.connect(); // Connect to adapter and initialize
await client.disconnect(); // Disconnect from adapter
await client.reset(); // Reset adapter using ATZ (independent from reconnect)
client.isConnected(); // Check connection status
client.getAdapterInfo(); // Get adapter information (version, protocol, device)// Generic query methods
await client.query(commandName); // Query by command name (e.g., 'ENGINE_RPM')
await client.queryPid(pid); // Query by PID string (e.g., '010C')
await client.queryMultiple([commands]); // Query multiple parameters sequentially
await client.queryCommand(command); // Query with a custom OBD2Command object
// Convenience methods
await client.getRPM(); // Engine RPM
await client.getSpeed(); // Vehicle speed (km/h)
await client.getCoolantTemperature(); // Coolant temperature (°C)
await client.getEngineLoad(); // Engine load (%)
await client.getFuelLevel(); // Fuel level (%)
await client.getThrottlePosition(); // Throttle position (%)
// Oxygen sensor methods (NEW!)
await client.query('O2S1_WR'); // O2 Sensor 1 Wide Range
await client.query('O2S2_WR'); // O2 Sensor 2 Wide Range
await client.query('O2S3_WR'); // O2 Sensor 3 Wide Range
await client.query('O2S4_WR'); // O2 Sensor 4 Wide Range
await client.query('O2S1_V'); // O2 Sensor 1 Voltage
await client.query('O2S2_V'); // O2 Sensor 2 Voltage
await client.query('O2S3_V'); // O2 Sensor 3 Voltage
await client.query('O2S4_V'); // O2 Sensor 4 Voltage
await client.query('O2S1_ST'); // O2 Sensor 1 Short Term Trimawait client.getDTCs(); // Get Diagnostic Trouble Codes (Mode 03)
await client.clearDTCs(); // Clear DTCs (Mode 04)
await client.getFreezeFrame(pid); // Get freeze frame data for specific PID (Mode 02)
await client.getAllFreezeFrames(); // Get all available freeze frame data
await client.getSupportedPids(); // Dynamically scan all supported PIDs
await client.scanPids(mode, start, end); // Scan PIDs in range with progress events
// Vehicle Information
await client.getVIN(); // Get Vehicle Identification Number (Mode 09)
await client.getCalibrationID(); // Get calibration ID
await client.getVehicleInfo(); // Get all vehicle info
await client.getProtocolInfo(); // Get protocol information
// Diagnostic Requests (OpenXC-inspired)
await client.sendDiagnosticRequest(config); // Send custom diagnostic requestawait client.startCANMonitor(); // Start monitoring all CAN traffic (AT MA)
await client.stopCANMonitor(); // Stop CAN monitoring
await client.startCANMonitorWithFilter('7E8'); // Monitor only frames matching CAN ID (AT CF + AT CM)
// Note: Use Flow Control configuration for controlled CAN communicationclient.setPollInterval(ms); // Set global poll interval (default: 1000ms)
client.addPoller(commandName); // Add command to polling list
client.startPolling(intervalMs); // Start polling all added commands
client.stopPolling(); // Stop polling
client.setAutoReconnect(enabled); // Enable/disable auto-reconnect with exponential backoffThe logger is disabled by default. Enable it to write logs to a file:
import { OBD2Client, LogFormat, LogLevel } from 'elm327';
const client = new OBD2Client(config);
// Enable logging with PRETTY format (NestJS-style)
client.enableLogger({
filePath: './obd2.log',
format: LogFormat.PRETTY,
});
// Or use RAW format (only the raw ELM327 response)
client.enableLogger({
filePath: './obd2-raw.log',
format: LogFormat.RAW,
});
// Or use JSON format (one JSON object per line)
client.enableLogger({
filePath: './obd2.json',
format: LogFormat.JSON,
});
// Filter by specific log levels
client.enableLogger({
filePath: './obd2-errors.log',
levels: [LogLevel.ERROR, LogLevel.WARN],
});
// Limit file size (keeps only the newest 2000 lines, trims oldest)
client.enableLogger({
filePath: './obd2.log',
format: LogFormat.JSON,
maxLines: 2000,
});
// Change maxLines at runtime
client.setLoggerMaxLines(5000);
// Change format at runtime
client.setLoggerFormat(LogFormat.JSON);
// Change levels at runtime
client.setLoggerLevels([LogLevel.INFO, LogLevel.ERROR]);
// Disable logging
client.disableLogger();| Format | Description | Example |
|---|---|---|
LogFormat.RAW |
Raw ELM327 response only | 41 0C 1A F8 |
LogFormat.PRETTY |
NestJS-style formatted log | [2026-05-07 10:30:45] CMD [OBD2Client] 010C {"name":"ENGINE_RPM"} |
LogFormat.JSON |
One JSON object per line | {"timestamp":"2026-05-07T10:30:45.000Z","level":"CMD","context":"OBD2Client","message":"010C","name":"ENGINE_RPM"} |
| Level | Description |
|---|---|
LogLevel.INFO |
General information (connect, disconnect, etc.) |
LogLevel.DEBUG |
Debug information |
LogLevel.WARN |
Warning messages |
LogLevel.ERROR |
Error messages |
LogLevel.RAW_DATA |
Raw data from adapter |
LogLevel.COMMAND |
Commands sent to adapter |
LogLevel.RESPONSE |
Responses received from adapter |
client.on('connected', () => {}); // Connection established
client.on('disconnected', () => {}); // Connection lost
client.on('ready', (adapterInfo) => {}); // Adapter initialized successfully
client.on('response', (response) => {}); // Decoded data received
client.on('error', (error) => {}); // Error occurred
client.on('rawData', (data) => {}); // Raw data from adapter
client.on('pollData', (data) => {}); // Polling data received
client.on('pollError', (command, error) => {}); // Polling error
client.on('pollComplete', (results) => {}); // Polling complete
client.on('scanProgress', (data) => {}); // PID scan progress updates
client.on('scanComplete', (data) => {}); // PID scan completed
client.on('reconnecting', () => {}); // Reconnection in progress
client.on('reconnected', () => {}); // Reconnection successful
client.on('canData', (data) => {}); // CAN frame received (monitor mode)
client.on('adapterReset', () => {}); // Adapter reset completed
client.on('debug', (data) => {}); // Debug information| Command | Description | Unit |
|---|---|---|
| ENGINE_LOAD | Calculated engine load | % |
| COOLANT_TEMP | Engine coolant temperature | °C |
| FUEL_PRESSURE | Fuel pressure | kPa |
| INTAKE_PRESSURE | Intake manifold absolute pressure | kPa |
| ENGINE_RPM | Engine speed | rpm |
| VEHICLE_SPEED | Vehicle speed | km/h |
| TIMING_ADVANCE | Timing advance | ° |
| INTAKE_TEMP | Intake air temperature | °C |
| MAF_RATE | Mass air flow sensor air flow rate | g/s |
| THROTTLE_POS | Absolute throttle position | % |
| OBD_STANDARDS | OBD standards compliance | - |
| RUNTIME | Run time since engine start | seconds |
| FUEL_LEVEL | Fuel tank level input | % |
| BAROMETRIC_PRESSURE | Absolute barometric pressure | kPa |
| AMBIENT_TEMP | Ambient air temperature | °C |
| VIN | Vehicle Identification Number | - |
| O2S1_WR | O2 Sensor 1 Wide Range | - |
| O2S2_WR | O2 Sensor 2 Wide Range | - |
| O2S3_WR | O2 Sensor 3 Wide Range | - |
| O2S4_WR | O2 Sensor 4 Wide Range | - |
| O2S1_V | O2 Sensor 1 Voltage | V |
| O2S2_V | O2 Sensor 2 Voltage | V |
| O2S3_V | O2 Sensor 3 Voltage | V |
| O2S4_V | O2 Sensor 4 Voltage | V |
| O2S1_ST | O2 Sensor 1 Short Term Trim | % |
import { listSerialPorts, isBluetoothAvailable, getAllCommands, createOBD2Client } from 'elm327';
// List available serial ports
const ports = await listSerialPorts();
// Check if Bluetooth is available (browser only)
const btAvailable = await isBluetoothAvailable();
// Get all predefined OBD2 commands
const commands = getAllCommands();
// Create client with convenience function
const client = createOBD2Client(config);- ELM327-based adapters (USB, Bluetooth, WiFi)
- OBDLink adapters
- UniCarScan adapters
- Generic OBD2 interfaces
- ELM327 USB
- ELM327 Bluetooth
- Vgate iCar Pro Bluetooth
- BAFX Products Bluetooth OBD2
- Generic ELM327 WiFi adapters
-
Old clones (v1.5/v2.1) with
cloneCompatibilitymode
- Most reliable connection method
- Typically uses
/dev/ttyUSB0on Linux,COM3on Windows - Standard baud rates: 9600, 38400, 115200
- For old clones: use
cloneCompatibility: 'lenient'or'minimal'
- In browsers: uses Web Bluetooth API (BLE only) - no address needed (uses UUID scan)
- In Node.js: use SerialConnection with a paired device
-
Linux:
rfcomm connect /dev/rfcomm0 <MAC>then use SerialConnection -
macOS: use
/dev/tty.*device after pairing - Smart Discovery: Automatically tries multiple known UUIDs for clone support
-
Note: For Web Bluetooth,
addressis optional (scans for devices automatically)
- Connects over TCP/IP to WiFi ELM327 adapters
- Default: 192.168.0.10:35000
- Requires connecting to the adapter's WiFi network first
The library includes several examples in the examples/ directory:
npm run build
# Auto-detect serial port:
npm run example:basic
# Or specify a port:
npm run example:basic -- /dev/ttyUSB0# Real-time monitoring (port required):
npm run example:monitoring -- /dev/ttyUSB0npm run example:wifinpx ts-node examples/flow-control.ts /dev/ttyUSB0Demonstrates CAN flow control (AT FC SH/SD/SM/CFC) for controlled communication.
npx ts-node examples/can-monitor.ts /dev/ttyUSB0Captures raw CAN traffic using AT MA (Monitor All) command.
npx ts-node examples/can-monitor-with-filter.ts 7E8Monitors only frames matching the specified CAN ID (uses AT CF + AT CM commands).
npx ts-node examples/pid-scanner.ts /dev/ttyUSB0Dynamically scans all supported PIDs with progress events.
npx ts-node examples/clone-compat.ts /dev/ttyUSB0 lenientDemonstrates clone compatibility modes for old ELM327 v1.5/v2.1 adapters.
npx ts-node examples/reset-adapter.ts /dev/ttyUSB0Shows how to reset the adapter independently using ATZ without reconnecting.
npx ts-node examples/bluetooth-ble.tsDemonstrates BLE smart discovery with multiple UUID patterns.
npx ts-node examples/freeze-frame.ts /dev/ttyUSB0Reads freeze frame data (Mode 02) captured at the moment of fault.
const { OBD2Client } = require('elm327');
const client = new OBD2Client({
type: 'serial',
port: '/dev/ttyUSB0',
});
await client.connect();
// Monitor key parameters every 2 seconds
setInterval(async () => {
try {
const data = await client.queryMultiple([
'ENGINE_RPM',
'VEHICLE_SPEED',
'COOLANT_TEMP',
'ENGINE_LOAD',
]);
console.log('Vehicle Data:', data);
} catch (error) {
console.error('Monitoring error:', error.message);
}
}, 2000);const { OBD2Client, ConnectionError, TimeoutError, ProtocolError } = require('elm327');
const client = new OBD2Client(config);
client.on('error', (error) => {
if (error.code === 'CONNECTION_ERROR') {
console.log('Connection lost, attempting to reconnect...');
} else if (error.code === 'TIMEOUT_ERROR') {
console.log('Command timed out');
} else if (error.code === 'PROTOCOL_ERROR') {
console.log('Protocol error:', error.message);
}
});
try {
await client.connect();
} catch (error) {
console.error('Failed to connect:', error.message);
}The library now parses NRC (Negative Response Codes) from diagnostic requests and provides human-readable messages:
const response = await client.sendDiagnosticRequest({
mode: DiagnosticMode.CURRENT_DATA,
pid: 0x0d,
});
if (!response.success && response.negativeResponseCode) {
console.log(`NRC: ${response.negativeResponseMessage}`);
// Example: "Request Out of Range (0x31)"
}import { OBD2Client, OBD2Command } from 'elm327';
// Define a custom command
const customCommand: OBD2Command = {
name: 'CUSTOM_PARAM',
pid: '0150',
description: 'Custom parameter',
decoder: (data: string) => {
const value = parseInt(data.substring(4, 6), 16);
return value * 0.5;
},
unit: 'custom_unit',
};
const client = new OBD2Client(config);
await client.connect();
const response = await client.queryCommand(customCommand);
console.log(`Custom param: ${response.value} ${response.unit}`);import { OBD2Client, LogFormat, LogLevel } from 'elm327';
const client = new OBD2Client({
type: 'serial',
port: '/dev/ttyUSB0',
});
// Enable JSON logging
client.enableLogger({
filePath: './logs/obd2-session.json',
format: LogFormat.JSON,
});
await client.connect();
// All commands and responses will be logged
const rpm = await client.getRPM();
const dtcs = await client.getDTCs();
await client.disconnect();
// Logger is automatically disabled on disconnectsudo chmod 666 /dev/ttyUSB0
# or add user to dialout group
sudo usermod -a -G dialout $USER- Check if adapter is properly connected
- Use
listSerialPorts()to find available ports - Try different USB ports
- Verify adapter compatibility (ELM327 recommended)
- Check baud rate settings
- Ensure vehicle is running or ignition is on
- For old clones, try
cloneCompatibility: 'lenient'with longer timeout (10000ms+)
- Pair adapter with system first
- Check if adapter is already connected to another device
- Verify Bluetooth permissions
- Try BLE smart discovery (multiple UUIDs supported)
- Ensure you are connected to the adapter's WiFi network
- Verify IP address (default: 192.168.0.10) and port (default: 35000)
- Check firewall settings
- Use sequential queries instead of parallel (
queryMultipleis sequential by design) - Increase timeout values
- Use
cloneCompatibility: 'minimal'mode
Enable debug logging using the events:
const client = new OBD2Client(config);
client.on('rawData', (data) => {
console.log('Raw data:', data);
});
client.on('debug', (data) => {
console.log('Debug:', data.message);
});
client.on('error', (error) => {
console.error('Debug error:', error);
});Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
See CHANGELOG.md for version history and changes.
git clone https://github.com/edu-amr/elm327.git
cd elm327
npm install
npm run build
npm testnpm run build
# Auto-detect serial port:
npm run example:basic
# Or specify a port:
npm run example:basic -- /dev/ttyUSB0
# Real-time monitoring (port required):
npm run example:monitoring -- /dev/ttyUSB0
# New examples:
npx ts-node examples/flow-control.ts /dev/ttyUSB0
npx ts-node examples/can-monitor.ts /dev/ttyUSB0
npx ts-node examples/pid-scanner.ts /dev/ttyUSB0
npx ts-node examples/clone-compat.ts /dev/ttyUSB0 lenient
npx ts-node examples/reset-adapter.ts /dev/ttyUSB0
npx ts-node examples/freeze-frame.ts /dev/ttyUSB0This project uses Husky for git hooks:
-
pre-commit: Runs typecheck (
tsc --noEmit) on staged.tsfiles - commit-msg: Validates commit messages using commitlint (conventional commits)
# Hooks are automatically installed after npm install
# To skip hooks (not recommended): git commit --no-verifyThis project is licensed under the MIT License — see the LICENSE file for details.
- Based on the ELM327 command set
- Inspired by the OBD2 protocol specifications
- Thanks to the automotive diagnostics community
- OpenXC project for diagnostic request inspiration
- python-OBD — Python OBD2 library
- node-obd — Another Node.js OBD library
- elm327-emulator — ELM327 emulator for testing
For issues, questions, or contributions, please visit the GitHub repository.