elm327
Version:
Node.js/TypeScript library for ELM327 OBD2 adapters over USB, Bluetooth and WiFi
222 lines • 9.03 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BluetoothConnection = void 0;
const connection_1 = require("./connection");
const errors_1 = require("./errors");
/**
* Known ELM327 BLE UUIDs for smart discovery.
* Cheap clones use different UUIDs than the standard ELM327.
*/
const ELM327_BLE_UUIDS = {
// Standard ELM327 BLE UUIDs
STANDARD: {
service: '0000fff0-0000-1000-8000-00805f9b34fb',
writeCharacteristic: '0000fff1-0000-1000-8000-00805f9b34fb',
notifyCharacteristic: '0000fff1-0000-1000-8000-00805f9b34fb',
},
// Common clone UUIDs
CLONE_FFE0: {
service: '0000ffe0-0000-1000-8000-00805f9b34fb',
writeCharacteristic: '0000ffe1-0000-1000-8000-00805f9b34fb',
notifyCharacteristic: '0000ffe1-0000-1000-8000-00805f9b34fb',
},
CLONE_FFF0: {
service: '0000fff0-0000-1000-8000-00805f9b34fb',
writeCharacteristic: '0000fff1-0000-1000-8000-00805f9b34fb',
notifyCharacteristic: '0000fff2-0000-1000-8000-00805f9b34fb',
},
CLONE_BEEF: {
service: '0000beef-0000-1000-8000-00805f9b34fb',
writeCharacteristic: '0000beef-0000-1000-8000-00805f9b34fb',
notifyCharacteristic: '0000beef-0000-1000-8000-00805f9b34fb',
},
CLONE_FFE0_FFE1: {
service: '0000ffe0-0000-1000-8000-00805f9b34fb',
writeCharacteristic: '0000ffe1-0000-1000-8000-00805f9b34fb',
notifyCharacteristic: '0000ffe2-0000-1000-8000-00805f9b34fb',
},
};
/**
* Bluetooth connection to an ELM327 adapter.
*
* In browsers: uses the Web Bluetooth API (BLE only).
* In Node.js: not natively supported — use SerialConnection with a paired
* device via rfcomm (Linux: /dev/rfcomm0) or /dev/tty.* (macOS).
*
* Updated to use ResponseMatcher for better request/response matching.
* Implements smart discovery with multiple known UUIDs for clone compatibility.
*/
class BluetoothConnection extends connection_1.OBD2Connection {
socket = null;
buffer = '';
_btHandler = undefined;
_characteristic = null;
_device = null;
constructor(config) {
super(config);
if (!config.address) {
throw new Error('Bluetooth address is required for Bluetooth connections');
}
}
async connect() {
try {
if (this.hasWebBluetooth()) {
await this.connectWebBluetooth();
}
else {
await this.connectNativeBluetooth();
}
this.isConnected = true;
this.emit('connected');
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new errors_1.ConnectionError(`Bluetooth connection failed: ${message}`);
}
}
async disconnect() {
this.rejectAllPending(new errors_1.ConnectionError('Disconnected'));
if (this.socket) {
try {
// Remove BLE listener if it exists
if (this._characteristic && this._btHandler) {
this._characteristic.removeEventListener('characteristicvaluechanged', this._btHandler);
this._characteristic = undefined;
this._btHandler = undefined;
}
if (typeof this.socket.close === 'function')
this.socket.close();
else if (typeof this.socket.disconnect === 'function')
this.socket.disconnect();
}
catch {
// ignore disconnect errors
}
this.socket = null;
}
this._device = null;
this.isConnected = false;
this.emit('disconnected');
}
async sendRaw(data) {
if (!this.socket) {
throw new errors_1.ConnectionError('Not connected via Bluetooth');
}
// Check if still connected before sending
if (!this.isConnected) {
throw new errors_1.ConnectionError('Connection lost before sending data');
}
const cmd = data + '\r';
const socket = this.socket;
try {
await socket.send(cmd);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new errors_1.ConnectionError(`Failed to send data: ${message}`);
}
}
isConnectionOpen() {
return this.socket !== null && this.isConnected;
}
clearBuffer() {
this.buffer = '';
}
hasWebBluetooth() {
return (typeof globalThis !== 'undefined' &&
'navigator' in globalThis &&
'bluetooth' in globalThis.navigator);
}
async connectWebBluetooth() {
const bt = globalThis.navigator.bluetooth;
if (!bt) {
throw new Error('Web Bluetooth API is not available');
}
// Smart discovery: try all known ELM327 UUIDs
const uuidEntries = Object.entries(ELM327_BLE_UUIDS);
let lastError = null;
for (const [name, uuids] of uuidEntries) {
try {
this.emit('debug', { message: `Trying BLE UUID: ${name}` });
const filters = [{ services: [uuids.service] }];
const optionalServices = [uuids.service];
// Request device with current UUID
const device = await bt.requestDevice({
filters,
optionalServices,
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(uuids.service);
const characteristic = await service.getCharacteristic(uuids.notifyCharacteristic);
await characteristic.startNotifications();
// Save reference for later removal
this._btHandler = this.handleBluetoothData.bind(this);
this._characteristic = characteristic;
this._device = device;
characteristic.addEventListener('characteristicvaluechanged', this._btHandler);
this.socket = {
send: (data) => {
const encoder = new TextEncoder();
const writeChar = uuids.writeCharacteristic;
// Use writeCharacteristic if different from notifyCharacteristic
if (writeChar !== uuids.notifyCharacteristic) {
return service
.getCharacteristic(writeChar)
.then((wc) => wc.writeValue(encoder.encode(data)));
}
return characteristic.writeValue(encoder.encode(data));
},
close: () => device.gatt.disconnect(),
};
this.emit('debug', { message: `Connected using UUID: ${name}` });
return; // Success, exit the loop
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
this.emit('debug', { message: `UUID ${name} failed: ${lastError.message}` });
// Continue to next UUID
}
}
// If we get here, all UUIDs failed
throw lastError || new Error('No ELM327 device found with known UUIDs');
}
async connectNativeBluetooth() {
throw new Error('Native Bluetooth is not supported in Node.js by this adapter. ' +
'Use SerialConnection with a paired device via rfcomm (Linux) ' +
'or /dev/tty.* (macOS).');
}
handleBluetoothData(event) {
const value = event.target.value;
if (!value)
return;
const decoder = new TextDecoder();
const data = decoder.decode(value);
this.buffer += data;
let idx;
while ((idx = this.buffer.indexOf('>')) !== -1) {
// Include the '>' prompt in the data passed to handleIncomingData
const raw = this.buffer.slice(0, idx + 1);
this.buffer = this.buffer.slice(idx + 1);
if (raw.trim().length > 0) {
// Send to ResponseMatcher for request matching (with '>' included)
this.handleIncomingData(raw);
// Also emit raw data event (without '>' for compatibility)
this.emit('data', raw.replace('>', '').trim());
}
}
}
/**
* Checks if Bluetooth is available in the current environment.
*/
static async isBluetoothAvailable() {
if (typeof globalThis !== 'undefined' && 'navigator' in globalThis) {
const nav = globalThis.navigator;
if ('bluetooth' in nav) {
return await nav.bluetooth.getAvailability();
}
}
return false;
}
}
exports.BluetoothConnection = BluetoothConnection;
//# sourceMappingURL=bluetooth-connection.js.map