elm327
Version:
Node.js/TypeScript library for ELM327 OBD2 adapters over USB, Bluetooth and WiFi
980 lines • 36.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OBD2Client = void 0;
const events_1 = require("events");
const bluetooth_connection_1 = require("./bluetooth-connection");
const commands_1 = require("./commands");
const errors_1 = require("./errors");
const logger_1 = require("./logger");
const serial_connection_1 = require("./serial-connection");
const types_1 = require("./types");
const wifi_connection_1 = require("./wifi-connection");
/**
* High-level OBD2 client for communicating with vehicles.
* Supports serial, Bluetooth, and WiFi connections to ELM327 adapters.
*/
class OBD2Client extends events_1.EventEmitter {
config;
connection;
adapterInfo;
isInitialized = false;
autoReconnect = false;
reconnectTimer;
_manualDisconnect = false;
pollers = new Map();
globalPollInterval;
pollIntervalMs = 1000; // Default 1 second
heartbeatTimer;
lastCommandTime = Date.now();
heartbeatIntervalMs = 20000; // 20 seconds
_canDataHandler;
logger = null;
constructor(config) {
super();
this.config = config;
}
/**
* Enables or disables auto-reconnect on connection loss.
*/
setAutoReconnect(enabled) {
this.autoReconnect = enabled;
}
enableLogger(config) {
this.logger = new logger_1.OBD2Logger(config);
this.logger.enable();
this.logger.info('OBD2Client', 'Logger enabled', {
filePath: config.filePath,
format: config.format ?? logger_1.LogFormat.PRETTY,
});
}
disableLogger() {
if (this.logger) {
this.logger.info('OBD2Client', 'Logger disabled');
this.logger.disable();
this.logger = null;
}
}
setLoggerFormat(format) {
this.logger?.setFormat(format);
}
setLoggerLevels(levels) {
this.logger?.setLevels(levels);
}
/**
* Starts the heartbeat timer to keep the connection alive.
* Sends a lightweight AT command every 20s if no other command is sent.
*/
startHeartbeat() {
this.stopHeartbeat();
this.lastCommandTime = Date.now();
this.heartbeatTimer = setInterval(() => {
const idleTime = Date.now() - this.lastCommandTime;
// If idle for more than heartbeat interval, send keep-alive
if (idleTime > this.heartbeatIntervalMs && this.isInitialized && this.connection) {
this.connection
.sendCommand('AT', 1000)
.then(() => {
this.lastCommandTime = Date.now();
})
.catch(() => {
// Ignore errors - heartbeat is best-effort
});
}
}, this.heartbeatIntervalMs);
}
/**
* Stops the heartbeat timer.
*/
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined;
}
}
/**
* Connects to the OBD2 adapter and initializes it.
*/
async connect() {
this.logger?.info('OBD2Client', 'Connecting to adapter', { type: this.config.type });
try {
if (this.connection) {
this.connection.removeAllListeners();
// Properly await disconnect before creating new connection
try {
await this.connection.disconnect();
}
catch {
// Ignore disconnect errors
}
this.connection = undefined;
this.isInitialized = false;
// Small delay to ensure port is fully released
await this.delay(500);
}
if (this.config.type === 'serial') {
this.connection = new serial_connection_1.SerialConnection(this.config);
}
else if (this.config.type === 'bluetooth') {
this.connection = new bluetooth_connection_1.BluetoothConnection(this.config);
}
else if (this.config.type === 'wifi') {
this.connection = new wifi_connection_1.WifiConnection(this.config);
}
else {
throw new Error(`Unsupported connection type: ${this.config.type}`);
}
this.connection.on('connected', () => this.emit('connected'));
this.connection.on('disconnected', () => {
this.emit('disconnected');
if (this.autoReconnect && !this._manualDisconnect && !this.reconnectTimer) {
this.emit('reconnecting');
// Exponential backoff: 1s, 2s, 4s, 8s... up to 30s
const baseDelay = 1000;
const maxDelay = 30000;
const maxAttempts = 10; // Limit reconnection attempts
let attempt = 0;
const attemptReconnect = async () => {
try {
await this.connect();
this.reconnectTimer = undefined;
this._manualDisconnect = false;
attempt = 0; // Reset on success
this.emit('reconnected');
}
catch (error) {
attempt++;
const message = error instanceof Error ? error.message : String(error);
// Stop if vehicle is off (UNABLE TO CONNECT)
if (message.includes('UNABLE TO CONNECT') || message.includes('Vehicle not responding')) {
this.emit('error', new errors_1.ConnectionError('Vehicle appears to be off. Reconnection stopped.'));
return;
}
// Stop after max attempts
if (attempt >= maxAttempts) {
this.emit('error', new errors_1.ConnectionError(`Reconnection failed after ${maxAttempts} attempts.`));
return;
}
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = undefined;
attemptReconnect();
}, delay);
}
};
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = undefined;
attemptReconnect();
}, baseDelay);
}
this._manualDisconnect = false;
});
this.connection.on('error', (error) => this.emit('error', error));
this.connection.on('data', (data) => this.emit('rawData', data));
await this.connection.connect();
this.adapterInfo = await this.connection.initialize();
this.isInitialized = true;
this.logger?.info('OBD2Client', 'Connected and initialized', {
protocol: this.adapterInfo.protocol,
version: this.adapterInfo.version,
device: this.adapterInfo.device,
});
// Start heartbeat to prevent WiFi/Bluetooth disconnection
this.startHeartbeat();
this.emit('ready', this.adapterInfo);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger?.error('OBD2Client', 'Connection failed', { error: message });
throw new errors_1.ConnectionError(`Connection failed: ${message}`);
}
}
/**
* Sets the default polling interval for all pollers.
*/
setPollInterval(ms) {
this.pollIntervalMs = ms;
}
/**
* Adds a command to the polling list.
* Similar to bluetooth-obd's addPoller("rpm").
*/
addPoller(commandName) {
if (!this.pollers.has(commandName)) {
this.pollers.set(commandName, {
interval: undefined,
intervalMs: this.pollIntervalMs,
});
}
}
/**
* Removes a command from the polling list.
*/
removePoller(commandName) {
const poller = this.pollers.get(commandName);
if (poller && poller.interval) {
clearInterval(poller.interval);
}
this.pollers.delete(commandName);
}
/**
* Starts automatic polling at the specified interval.
* Similar to serial-obd's startPolling(1000).
*/
startPolling(intervalMs) {
const interval = intervalMs || this.pollIntervalMs;
// Clear existing global poll
if (this.globalPollInterval) {
clearInterval(this.globalPollInterval);
}
this.globalPollInterval = setInterval(async () => {
if (!this.isInitialized || !this.isConnected()) {
return;
}
const commands = Array.from(this.pollers.keys());
if (commands.length === 0) {
// If no specific pollers, use default set
commands.push('ENGINE_RPM', 'VEHICLE_SPEED', 'COOLANT_TEMP');
}
try {
const results = await this.queryMultiple(commands);
for (const r of results) {
if ('error' in r) {
this.emit('pollError', r.command, r.error);
}
else {
this.emit('pollData', r);
}
}
this.emit('pollComplete', results);
}
catch (error) {
this.emit('pollError', 'POLL_ERROR', error instanceof Error ? error.message : error);
}
}, interval);
}
/**
* Stops the automatic polling.
*/
stopPolling() {
if (this.globalPollInterval) {
clearInterval(this.globalPollInterval);
this.globalPollInterval = undefined;
}
// Also clear individual pollers
for (const [name, poller] of Array.from(this.pollers.entries())) {
if (poller.interval) {
clearInterval(poller.interval);
poller.interval = undefined;
}
}
}
/**
* Disconnects from the OBD2 adapter.
*/
async disconnect() {
this.logger?.info('OBD2Client', 'Disconnecting from adapter');
this.stopPolling(); // Stop polling on disconnect
this.stopHeartbeat(); // Stop heartbeat
this._manualDisconnect = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;
}
if (this.connection) {
await this.connection.disconnect();
this.connection = undefined;
this.isInitialized = false;
}
}
/**
* Resets the adapter using ATZ command without disconnecting/reconnecting.
* Useful for recovering from communication errors or resetting adapter state.
* This is an independent reset that doesn't recreate the socket/connection.
*
* @example
* try {
* await client.query('ENGINE_RPM');
* } catch (error) {
* console.log('Error, resetting adapter...');
* await client.reset(); // Reset without full reconnect
* await client.query('ENGINE_RPM'); // Try again
* }
*/
async reset() {
if (!this.isInitialized) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
if (!this.connection) {
throw new errors_1.ConnectionError('Not connected to OBD2 adapter');
}
try {
await this.connection.reset();
this.emit('adapterReset');
this.logger?.info('OBD2Client', 'Adapter reset successful');
}
catch (error) {
this.logger?.error('OBD2Client', 'Failed to reset adapter', {
error: error instanceof Error ? error.message : String(error),
});
this.emit('error', error instanceof Error ? error : new Error(String(error)));
throw new errors_1.ConnectionError(`Failed to reset adapter: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Queries a command by its name (e.g., 'ENGINE_RPM').
*/
async query(commandName) {
this.logger?.logCommand('OBD2Client', commandName);
if (!this.isInitialized) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
if (!this.isConnected()) {
throw new errors_1.ConnectionError('Not connected to OBD2 adapter');
}
const command = commands_1.OBD2_COMMANDS[commandName];
if (!command) {
throw new Error(`Unknown command: ${commandName}`);
}
this.lastCommandTime = Date.now(); // Update for heartbeat
return this.queryCommand(command);
}
/**
* Queries a command by its PID string (e.g., '010C').
*/
async queryPid(pid) {
if (!this.isInitialized) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
if (!this.isConnected()) {
throw new errors_1.ConnectionError('Not connected to OBD2 adapter');
}
const command = (0, commands_1.getCommandByPid)(pid);
if (!command) {
throw new Error(`Unknown PID: ${pid}`);
}
this.lastCommandTime = Date.now(); // Update for heartbeat
return this.queryCommand(command);
}
/**
* Sends a command to the adapter and decodes the response.
*/
async queryCommand(command) {
if (!this.isInitialized) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
if (!this.connection) {
throw new errors_1.ConnectionError('Not connected to OBD2 adapter');
}
try {
this.lastCommandTime = Date.now(); // Update for heartbeat
this.logger?.logCommand('OBD2Client', command.pid, { name: command.name });
const response = await this.connection.sendCommand(command.pid);
this.logger?.logResponse('OBD2Client', response, { command: command.name });
const value = command.decoder(response);
const result = {
command: command.name,
value,
unit: command.unit,
timestamp: new Date(),
};
this.emit('response', result);
return result;
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger?.error('OBD2Client', `Failed to query ${command.name}`, { error: message });
throw new errors_1.ProtocolError(`Failed to query ${command.name}: ${message}`);
}
}
/**
* Queries multiple commands sequentially.
* Sequential execution is intentional to avoid BUFFER FULL on cheap clones.
* Returns array with either OBD2Response or error info.
*/
async queryMultiple(commandNames) {
const results = [];
for (const commandName of commandNames) {
try {
const result = await this.query(commandName);
results.push(result);
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
results.push({ command: commandName, error: message });
this.emit('error', error);
}
await this.delay(100);
}
return results;
}
/**
* Gets vehicle information including VIN and adapter details.
*/
async getVehicleInfo() {
const info = {};
try {
const vin = await this.query('VIN');
info.vin = vin.value;
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
info.vin = 'Not available';
info.vinError = { error: message };
}
try {
const standards = await this.query('OBD_STANDARDS');
info.obdStandards = standards.value;
}
catch {
// ignore if not supported
}
if (this.adapterInfo) {
info.adapter = this.adapterInfo;
}
return info;
}
/**
* Returns whether the client is connected and initialized.
*/
isConnected() {
return this.connection?.getConnectionStatus() || false;
}
/**
* Returns information about the connected adapter.
*/
getAdapterInfo() {
return this.adapterInfo;
}
/**
* Returns all available command names.
*/
getAvailableCommands() {
return Object.keys(commands_1.OBD2_COMMANDS);
}
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Extracts base PID from a query string like "0100", "0120", etc.
* Returns the numeric base PID (e.g., 0x00, 0x20, 0x40...).
*/
getBasePid(query) {
// query is like "0100", "0120", etc. - extract the last two chars as hex
const baseHex = query.substring(2); // Remove mode "01"
return parseInt(baseHex, 16);
}
/**
* Parses supported PIDs from a Mode 1 PID 00 response.
* Uses getBasePid for clarity.
*/
parseSupportedPids(response, baseQuery) {
const supportedPids = [];
const cleanResponse = response.replace(/[\r\n>]/g, '').replace(/\s/g, '');
// Extract data portion (skip "41" + PID byte = 4 chars total)
// Response format: 41[PID][data...] -> skip first 4 chars (41 + 2-char PID)
const dataStart = 4; // Skip "41" + PID (e.g., "4100", "4120", etc.)
const data = cleanResponse.substring(dataStart);
// Validate minimum length (need at least 8 hex chars = 4 bytes)
if (data.length < 8) {
return [];
}
const hex = data.substring(0, 8);
let binary = '';
for (let i = 0; i < hex.length; i++) {
const digit = parseInt(hex[i], 16);
if (isNaN(digit))
continue;
binary += digit.toString(2).padStart(4, '0');
}
const basePid = this.getBasePid(baseQuery);
for (let i = 0; i < binary.length; i++) {
if (binary[i] === '1') {
const pidNumber = basePid + i + 1;
supportedPids.push(pidNumber.toString(16).toUpperCase().padStart(2, '0'));
}
}
return supportedPids;
}
/**
* Gets the current engine RPM.
*/
async getRPM() {
return (await this.query('ENGINE_RPM')).value;
}
/**
* Gets the current vehicle speed in km/h.
*/
async getSpeed() {
return (await this.query('VEHICLE_SPEED')).value;
}
/**
* Gets the engine coolant temperature in °C.
*/
async getCoolantTemperature() {
return (await this.query('COOLANT_TEMP')).value;
}
/**
* Gets the calculated engine load as a percentage.
*/
async getEngineLoad() {
return (await this.query('ENGINE_LOAD')).value;
}
/**
* Gets the fuel tank level as a percentage.
*/
async getFuelLevel() {
return (await this.query('FUEL_LEVEL')).value;
}
/**
* Gets the absolute throttle position as a percentage.
*/
async getThrottlePosition() {
return (await this.query('THROTTLE_POS')).value;
}
/**
* Sends a custom diagnostic request using DiagnosticRequestConfig.
* Similar to OpenXC's create_diagnostic_request method.
*/
async sendDiagnosticRequest(config) {
if (!this.isInitialized) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
if (!this.connection) {
throw new errors_1.ConnectionError('Not connected to OBD2 adapter');
}
try {
this.lastCommandTime = Date.now(); // Update for heartbeat
this.logger?.logCommand('OBD2Client', `Mode ${config.mode} PID 0x${(config.pid ?? 0).toString(16).toUpperCase()}`, {
name: config.name,
});
const response = await this.connection.sendDiagnosticRequest(config);
if (!response) {
throw new errors_1.ProtocolError('No response received from diagnostic request');
}
this.logger?.logResponse('OBD2Client', JSON.stringify(response), {
success: response.success,
});
return response;
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger?.error('OBD2Client', 'Diagnostic request failed', { error: message });
throw new errors_1.ProtocolError(`Diagnostic request failed: ${message}`);
}
}
/**
* Sends a Mode 1 request (current data) for a specific PID.
* Convenience method for common diagnostic requests.
*/
async queryMode1(pid) {
return this.sendDiagnosticRequest({
mode: types_1.DiagnosticMode.CURRENT_DATA,
pid,
name: `Mode 1 PID 0x${pid.toString(16).toUpperCase()}`,
});
}
/**
* Gets the VIN using Mode 9 PID 02.
* Similar to OpenXC's get_vin method.
*/
async getVIN() {
try {
const response = await this.query('VIN');
return response.value;
}
catch {
// Fallback to custom diagnostic request
const response = await this.sendDiagnosticRequest({
mode: types_1.DiagnosticMode.VEHICLE_INFO,
pid: 0x02,
name: 'VIN',
});
if (response.payload) {
// VIN is ASCII encoded in the payload
const bytes = response.payload.match(/.{1,2}/g) || [];
return bytes
.map((b) => String.fromCharCode(parseInt(b, 16)))
.join('')
.trim();
}
return 'Not available';
}
}
/**
* Gets the calibration ID (Mode 9 PID 04).
*/
async getCalibrationID() {
const response = await this.sendDiagnosticRequest({
mode: types_1.DiagnosticMode.VEHICLE_INFO,
pid: 0x04,
name: 'Calibration ID',
});
if (response.payload) {
const bytes = response.payload.match(/.{1,2}/g) || [];
return bytes
.map((b) => String.fromCharCode(parseInt(b, 16)))
.join('')
.trim();
}
return 'Not available';
}
/**
* Scans all OBD-II PIDs to see which ones respond.
* Similar to OpenXC's openxc-obd2scanner tool.
*
* @param mode - The diagnostic mode (default: 0x01 for current data)
* @param startPid - Starting PID to scan (default: 0x00)
* @param endPid - Ending PID to scan (default: 0x80)
* @param onProgress - Optional callback for progress updates
*
* @emits scanProgress with { pid, response } when each PID is tested
* @emits scanComplete when scanning is finished
*/
async scanPids(mode = 0x01, startPid = 0x00, endPid = 0x80, onProgress) {
const results = new Map();
for (let pid = startPid; pid < endPid; pid++) {
try {
const response = await this.sendDiagnosticRequest({
mode,
pid,
});
if (response.success) {
results.set(pid, response);
}
// Emit progress event (for EventEmitter listeners)
this.emit('scanProgress', {
pid,
response: response.success ? response : null,
});
// Also call the callback if provided
if (onProgress) {
onProgress(pid, response.success ? response : null);
}
}
catch {
// Emit progress event even on error
this.emit('scanProgress', {
pid,
response: null,
});
if (onProgress) {
onProgress(pid, null);
}
}
await this.delay(50); // Small delay between requests
}
// Emit scan complete event
this.emit('scanComplete', {
totalScanned: endPid - startPid,
found: results.size,
results,
});
return results;
}
/**
* Gets all DTCs (Diagnostic Trouble Codes) using Mode 3.
*/
async getDTCs() {
if (!this.isInitialized || !this.connection) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
try {
this.logger?.logCommand('OBD2Client', '03', { name: 'Get DTCs' });
const response = await this.connection.sendCommand('03');
const dtcs = this.parseDTCs(response);
this.logger?.logResponse('OBD2Client', response, { dtcCount: dtcs.length });
return dtcs;
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger?.error('OBD2Client', 'Failed to get DTCs', { error: message });
throw new errors_1.ProtocolError(`Failed to get DTCs: ${message}`);
}
}
/**
* Gets freeze frame data for a specific PID (Mode 02).
* Freeze frame captures data at the moment a fault occurred.
*
* @param pid - The PID to get freeze frame data for (e.g., 0x0C for RPM)
* @returns The freeze frame value, or null if not available
*/
async getFreezeFrame(pid) {
if (!this.isInitialized || !this.connection) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
try {
const response = await this.sendDiagnosticRequest({
mode: types_1.DiagnosticMode.FREEZE_FRAME,
pid,
});
if (!response.success || !response.payload) {
return null;
}
// Parse the response using the command's decoder if available
// Use Mode 01 lookup (commands are defined as Mode 01, decoders are the same)
const pidHex = pid.toString(16).toUpperCase().padStart(2, '0');
const command = (0, commands_1.getCommandByPid)(`01${pidHex}`); // Mode 01 + PID (same decoder)
let value = 0;
let unit = '';
if (command && command.decoder) {
// Use the command's decoder
const decoded = command.decoder(response.payload);
value = decoded;
unit = command.unit || '';
}
else {
// Fallback: return raw payload
value = response.payload;
}
return {
command: `FREEZE_FRAME_${pidHex}`,
value,
unit,
timestamp: new Date(),
};
}
catch (error) {
this.emit('debug', {
message: `Freeze frame for PID 0x${pid.toString(16)} failed: ${error}`,
});
return null;
}
}
/**
* Gets all available freeze frame data.
* Scans PIDs 0x00-0x4F in Mode 02.
*
* @returns Array of freeze frame responses
*/
async getAllFreezeFrames() {
if (!this.isInitialized || !this.connection) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
const results = [];
// First, get supported PIDs in Mode 02 (PID 0x00)
try {
const supported = await this.sendDiagnosticRequest({
mode: types_1.DiagnosticMode.FREEZE_FRAME,
pid: 0x00,
});
if (supported.success && supported.payload) {
// Parse which PIDs are supported (Mode 02 uses "02" prefix)
const supportedPids = this.parseSupportedPids(supported.payload, '0200');
// Query each supported PID
for (const pidHex of supportedPids) {
const pid = parseInt(pidHex, 16);
const ff = await this.getFreezeFrame(pid);
if (ff) {
results.push(ff);
}
}
}
}
catch (error) {
this.emit('debug', { message: `Failed to get all freeze frames: ${error}` });
}
return results;
}
/**
* Dynamically scans all supported PIDs (Mode 01).
* Recursively checks 0x00, 0x20, 0x40, 0x60, etc.
*
* @returns Array of supported PID numbers
*/
async getSupportedPids() {
const allSupported = [];
// Start with PID 0x00, then recursively check 0x20, 0x40, etc.
for (let basePid = 0x00; basePid <= 0xE0; basePid += 0x20) {
try {
const response = await this.sendDiagnosticRequest({
mode: types_1.DiagnosticMode.CURRENT_DATA,
pid: basePid,
});
if (response.success && response.payload) {
// Convert basePid number to hex string for parseSupportedPids
const baseQuery = `01${basePid.toString(16).toUpperCase().padStart(2, '0')}`;
const supported = this.parseSupportedPids(response.payload, baseQuery);
// Convert string PIDs to numbers
const pidNumbers = supported.map((p) => parseInt(p, 16));
allSupported.push(...pidNumbers);
// If no more PIDs in this range, stop
if (supported.length === 0)
break;
}
}
catch {
// Stop on first failure
break;
}
}
return allSupported.sort((a, b) => a - b);
}
/**
* Clears all DTCs using Mode 4.
*/
async clearDTCs() {
if (!this.isInitialized || !this.connection) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
try {
this.logger?.logCommand('OBD2Client', '04', { name: 'Clear DTCs' });
await this.connection.sendCommand('04');
this.logger?.info('OBD2Client', 'DTCs cleared successfully');
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger?.error('OBD2Client', 'Failed to clear DTCs', { error: message });
throw new errors_1.ProtocolError(`Failed to clear DTCs: ${message}`);
}
}
/**
* Parses DTCs from a Mode 3 response.
* Uses proper byte pair matching.
*/
parseDTCs(response) {
const dtcs = [];
const clean = this.cleanResponse(response);
// Use proper byte pair matching (always 2 chars per byte)
const bytes = clean.match(/.{2}/g) || [];
// Skip mode response byte (43 = 0x40 + 3)
for (let i = 1; i < bytes.length - 1; i += 2) {
const byte1 = parseInt(bytes[i], 16);
const byte2 = parseInt(bytes[i + 1], 16);
if (byte1 === 0 && byte2 === 0)
break;
const code = this.decodeDTC(byte1, byte2);
if (code) {
dtcs.push(code);
}
}
return dtcs;
}
/**
* Decodes two bytes into a DTC code.
*/
decodeDTC(byte1, byte2) {
const firstChar = ['P', 'C', 'B', 'U'][(byte1 >> 6) & 0x3];
if (!firstChar)
return null;
const secondChar = ((byte1 >> 4) & 0x3).toString();
const thirdChar = (byte1 & 0xf).toString(16).toUpperCase();
const fourthChar = (byte2 >> 4).toString(16).toUpperCase();
const fifthChar = (byte2 & 0xf).toString(16).toUpperCase();
return `${firstChar}${secondChar}${thirdChar}${fourthChar}${fifthChar}`;
}
/**
* Gets adapter firmware version.
* Similar to OpenXC's version command.
*/
async getAdapterVersion() {
if (!this.adapterInfo) {
throw new errors_1.ConnectionError('Adapter not initialized');
}
return this.adapterInfo.version;
}
/**
* Gets protocol information.
*/
async getProtocolInfo() {
if (!this.adapterInfo) {
throw new errors_1.ConnectionError('Adapter not initialized');
}
return {
protocol: this.adapterInfo.protocol,
version: this.adapterInfo.version,
device: this.adapterInfo.device,
};
}
/**
* Sends raw AT command (for debugging).
*/
async sendRaw(command) {
if (!this.isInitialized) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
if (!this.connection) {
throw new errors_1.ConnectionError('Not connected to OBD2 adapter');
}
this.logger?.logCommand('OBD2Client', command, { raw: true });
const result = await this.connection.sendCommand(command);
this.logger?.logResponse('OBD2Client', result, { command });
return result;
}
/**
* Starts CAN bus monitoring (AT MA - Monitor All).
* Listens to all CAN traffic without sending requests.
* Data is emitted via the 'canData' event.
* Use stopCANMonitor() to exit monitor mode.
*
* @example
* client.on('canData', (data) => {
* console.log('CAN Frame:', data);
* });
* await client.startCANMonitor();
*/
async startCANMonitor() {
if (!this.isInitialized) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
if (!this.connection) {
throw new errors_1.ConnectionError('Not connected to OBD2 adapter');
}
// Save handler reference to remove later
this._canDataHandler = (data) => {
this.emit('canData', data);
};
// Forward canData events from connection
this.connection.on('canData', this._canDataHandler);
await this.connection.startMonitor();
}
/**
* Starts CAN monitoring with a specific CAN ID filter (AT MP + AT MA).
* Only frames matching the specified CAN ID will be received.
*
* @param canId - CAN ID to filter (e.g., '7E8', '7DF')
*/
async startCANMonitorWithFilter(canId) {
if (!this.isInitialized) {
throw new errors_1.ConnectionError('Adapter not initialized. Call connect() first.');
}
if (!this.connection) {
throw new errors_1.ConnectionError('Not connected to OBD2 adapter');
}
// Save handler reference to remove later
this._canDataHandler = (data) => {
this.emit('canData', data);
};
// Forward canData events from connection
this.connection.on('canData', this._canDataHandler);
await this.connection.startMonitorWithFilter(canId);
}
/**
* Stops CAN monitoring mode.
* Sends escape command to exit AT MA mode.
*/
async stopCANMonitor() {
if (!this.connection) {
return;
}
// Remove canData listener to avoid duplicates
if (this._canDataHandler) {
this.connection.off('canData', this._canDataHandler);
this._canDataHandler = undefined;
}
await this.connection.stopMonitor();
}
/**
* Clean response helper.
*/
cleanResponse(response) {
return response
.replace(/[\r\n>]/g, '')
.trim()
.toUpperCase()
.split(' ')
.filter((part) => part.length > 0)
.join('');
}
}
exports.OBD2Client = OBD2Client;
//# sourceMappingURL=obd2-client.js.map