UNPKG

elm327

Version:

Node.js/TypeScript library for ELM327 OBD2 adapters over USB, Bluetooth and WiFi

980 lines 36.9 kB
"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