UNPKG

elm327

Version:

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

183 lines 6.65 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ResponseMatcher = void 0; const events_1 = require("events"); const errors_1 = require("./errors"); /** * Matches incoming responses to pending requests. * Supports matching by command pattern or custom matching function. */ class ResponseMatcher extends events_1.EventEmitter { pendingRequests = new Map(); requestCounter = 0; destroyed = false; /** * Adds a new pending request and returns its ID. * If the matcher is destroyed (connection lost), rejects immediately. */ addRequest(command, timeout, matchFn) { // If destroyed, reject immediately without creating timer or incrementing counter if (this.destroyed) { const promise = Promise.reject(new errors_1.ProtocolError('Connection lost. Matcher is destroyed.')); return { id: 'destroyed', promise }; } const currentCount = this.requestCounter++; const id = `req_${Date.now()}_${currentCount}`; let resolveFn; let rejectFn; const promise = new Promise((resolve, reject) => { resolveFn = resolve; rejectFn = reject; }); const timer = setTimeout(() => { if (this.pendingRequests.has(id)) { this.pendingRequests.delete(id); rejectFn(new errors_1.TimeoutError(`Command timed out: ${command}`)); } }, timeout); const entry = { id, command, resolve: resolveFn, reject: rejectFn, timer, buffer: [], timestamp: Date.now(), }; if (matchFn) { entry.matchFn = matchFn; } this.pendingRequests.set(id, entry); return { id, promise }; } /** * Handles incoming data from the adapter. * Attempts to match the data to a pending request. * The data should include the '>' prompt for proper detection. */ handleData(data) { // If no pending requests, emit as unsolicited data if (this.pendingRequests.size === 0) { this.emit('unsolicited', data); return; } // Try to match against all pending requests for (const [id, request] of Array.from(this.pendingRequests.entries())) { // If a custom matcher is provided, use it if (request.matchFn && request.matchFn(data)) { this.resolveRequest(id, data); return; } } // Default: match the first pending request (FIFO) // This assumes ELM327 processes commands sequentially const firstId = this.pendingRequests.keys().next().value; if (firstId) { const request = this.pendingRequests.get(firstId); request.buffer.push(data); // Check if this looks like a complete response (has '>' prompt) // Also check if we have a complete response with proper mode byte const fullData = request.buffer.join('\n'); if (data.includes('>') && this.isCompleteResponse(fullData, request)) { const fullResponse = request.buffer.join('\n'); this.resolveRequest(firstId, fullResponse); } } } /** * Checks if the response appears to be complete. * Looks for the '>' prompt and valid response pattern. */ isCompleteResponse(data, request) { // Must have the '>' prompt if (!data.includes('>')) return false; // For ELM327, after '>' appears, the response is complete // Additional check: if we expect a specific response pattern, verify it const clean = data.replace(/[\r\n>]/g, '').trim(); if (clean.length === 0) return true; // Empty response with '>' is complete // Check for valid response (starts with 4x for successful, 7F for negative) const bytes = clean.split(/\s+/).filter((b) => b.length > 0); if (bytes.length > 0) { const firstByte = parseInt(bytes[0], 16); // Valid response modes: 0x40-0x4F (success) or 0x7F (negative) if ((firstByte >= 0x40 && firstByte <= 0x4f) || firstByte === 0x7f) { return true; } } return true; // Default: assume complete if '>' is present } /** * Resolves a pending request with the given response. */ resolveRequest(id, response) { const request = this.pendingRequests.get(id); if (!request) return; clearTimeout(request.timer); this.pendingRequests.delete(id); request.resolve(response); } /** * Rejects all pending requests with the given error. * Sets destroyed state to prevent new requests. */ rejectAll(error) { this.destroyed = true; for (const [, request] of Array.from(this.pendingRequests.entries())) { clearTimeout(request.timer); request.reject(error); } this.pendingRequests.clear(); } /** * Marks the matcher as destroyed (connection lost). * Future addRequest calls will be rejected immediately. */ destroy() { this.destroyed = true; this.rejectAll(new errors_1.ProtocolError('Connection lost.')); } /** * Resets the destroyed state (for reconnection). * Also resets the request counter to avoid overflow. */ reset() { this.destroyed = false; this.pendingRequests.clear(); this.requestCounter = 0; } /** * Gets the number of pending requests. */ get pendingCount() { return this.pendingRequests.size; } /** * Removes and rejects a specific request. */ cancelRequest(id) { const request = this.pendingRequests.get(id); if (request) { clearTimeout(request.timer); request.reject(new errors_1.ProtocolError(`Request cancelled: ${request.command}`)); this.pendingRequests.delete(id); } } /** * Clears the buffer of the first pending request. * Call this before sending a new command to avoid residual data. */ clearBuffer() { const firstId = this.pendingRequests.keys().next().value; if (firstId) { const request = this.pendingRequests.get(firstId); if (request) { request.buffer = []; } } } } exports.ResponseMatcher = ResponseMatcher; //# sourceMappingURL=response-matcher.js.map