elm327
Version:
Node.js/TypeScript library for ELM327 OBD2 adapters over USB, Bluetooth and WiFi
183 lines • 6.65 kB
JavaScript
"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