elm327
Version:
Node.js/TypeScript library for ELM327 OBD2 adapters over USB, Bluetooth and WiFi
238 lines • 9.47 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticResponseParser = exports.DiagnosticRequestBuilder = exports.DiagnosticMode = void 0;
const types_1 = require("./types");
Object.defineProperty(exports, "DiagnosticMode", { enumerable: true, get: function () { return types_1.DiagnosticMode; } });
/**
* Builds OBD-II diagnostic requests in hex format.
* Similar to OpenXC's DiagnosticRequest builder.
*/
class DiagnosticRequestBuilder {
config;
constructor(config) {
this.config = { ...config };
}
/**
* Builds the hex command string for this diagnostic request.
* Format: <mode><pid> (if pid is present)
*/
build() {
const modeHex = this.toHex(this.config.mode);
const pidHex = this.config.pid !== undefined ? this.toHex(this.config.pid) : '';
return modeHex + pidHex;
}
/**
* Returns atomic command for specific CAN ID.
* For ELM327, we combine AT SH + command as a single string
* to prevent header from being "forgotten" between commands.
* Uses semicolon to chain commands in ELM327.
*/
getCommandsForSpecificId() {
const modeHex = this.toHex(this.config.mode);
const pidHex = this.config.pid !== undefined ? this.toHex(this.config.pid) : '';
const command = modeHex + pidHex;
// If using specific ID (not 7DF broadcast), combine AT SH + command
if (this.config.id && this.config.id !== 0x7df) {
// Send as atomic command: AT SH + command in one line
// ELM327 will use the header for the next command only
return [`AT SH ${this.toHex(this.config.id)} ${command}`];
}
return [command];
}
/**
* Converts a number to 2-char hex string
*/
toHex(value) {
return value.toString(16).toUpperCase().padStart(2, '0');
}
/**
* Gets the configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Static helper to create a simple mode 1 request
*/
static mode1Request(pid, name) {
return new DiagnosticRequestBuilder({
mode: types_1.DiagnosticMode.CURRENT_DATA,
pid,
name: name || `Mode 1 PID 0x${pid.toString(16).toUpperCase()}`,
});
}
/**
* Static helper to create a vehicle info request (mode 9)
*/
static vehicleInfoRequest(pid) {
return new DiagnosticRequestBuilder({
mode: types_1.DiagnosticMode.VEHICLE_INFO,
pid,
});
}
/**
* Static helper to get VIN (Mode 9 PID 02)
*/
static vinRequest() {
return new DiagnosticRequestBuilder({
mode: types_1.DiagnosticMode.VEHICLE_INFO,
pid: 0x02,
name: 'VIN',
});
}
}
exports.DiagnosticRequestBuilder = DiagnosticRequestBuilder;
/**
* Parses raw ELM327 responses into DiagnosticResponse objects
*/
class DiagnosticResponseParser {
/**
* Parses a typical ELM327 response line
* Example: "41 0C 1A F8" (RPM response)
*
* Handles negative responses (7F) and extracts NRC codes.
* For multiple ECUs, parses each line separately.
*/
static parse(rawResponse, request) {
// Split by lines to handle multiple ECU responses
const lines = rawResponse.split(/[\r\n]+/).filter((l) => l.trim().length > 0);
// If multiple lines, parse each and return the first successful one
// or combine them appropriately
if (lines.length > 1) {
const responses = lines.map((line) => this.parseLine(line.trim(), request));
// Return the first successful response, or the first one if all failed
const successful = responses.find((r) => r.success);
return successful || responses[0];
}
return this.parseLine(rawResponse, request);
}
/**
* Parses a single response line
*/
static parseLine(rawResponse, request) {
const cleanResponse = rawResponse.replace(/[\r\n>]/g, '').trim();
const bytes = cleanResponse.split(/\s+/).filter((b) => b.length > 0);
const response = {
bus: request.bus || 1,
id: request.id || 0x7df,
mode: request.mode,
success: !cleanResponse.includes('NO DATA') && !cleanResponse.includes('ERROR'),
timestamp: new Date(),
};
if (request.pid !== undefined) {
response.pid = request.pid;
}
// Check for negative response (0x7F = mode + 0x40)
if (bytes.length > 0) {
const responseMode = parseInt(bytes[0], 16);
// Negative response: 7F XX YY (where YY is NRC)
if (responseMode === 0x7f) {
response.success = false;
// Extract NRC (Negative Response Code)
if (bytes.length > 1) {
response.pid = parseInt(bytes[1], 16); // Requested PID
}
if (bytes.length > 2) {
response.negativeResponseCode = parseInt(bytes[2], 16);
// Map common NRCs to human-readable messages
response.negativeResponseMessage = this.getNRCMessage(response.negativeResponseCode);
}
return response;
}
// Calculate mode (response mode - 0x40) and assign as number
const modeValue = responseMode - 0x40;
response.mode = modeValue;
// If there's a PID in the response
if (bytes.length > 1 && request.pid !== undefined) {
response.pid = parseInt(bytes[1], 16);
}
// Reconstruct payload
if (bytes.length > 2) {
response.payload = bytes.slice(2).join('');
}
}
return response;
}
/**
* Maps NRC (Negative Response Code) to human-readable message
*/
static getNRCMessage(nrc) {
const nrcMap = {
0x10: 'General reject',
0x11: 'Service not supported',
0x12: 'Sub-function not supported',
0x13: 'Incorrect message length or invalid format',
0x14: 'Response too long',
0x21: 'Busy - repeat request',
0x22: 'Conditions not correct or request sequence error',
0x23: 'Routine not complete or service in progress',
0x24: 'Request sequence error',
0x25: 'No response from sub-net component',
0x26: 'Failure prevents execution of requested action',
0x31: 'Request out of range',
0x33: 'Security access denied',
0x35: 'Invalid key',
0x36: 'Exceed number of attempts',
0x37: 'Required time delay not expired',
0x70: 'Upload/download not accepted',
0x71: 'Transfer data suspended',
0x72: 'General programming failure',
0x73: 'Wrong block sequence counter',
0x78: 'Request correctly received but response is pending',
0x7e: 'Sub-function not supported in active session',
0x7f: 'Service not supported in active session',
};
return nrcMap[nrc] || `Unknown NRC: 0x${nrc.toString(16).toUpperCase()}`;
}
/**
* Parses multiple responses (for broadcast requests)
*/
static parseMultipleResponses(rawResponse, request) {
const responses = [];
const cleanResponse = rawResponse.replace(/[\r\n>]/g, '').trim();
// Split by response mode pattern (4x where x is mode)
const responsePattern = /(4[0-9A-F][0-9A-F\s]*)/g;
const matches = cleanResponse.match(responsePattern);
if (matches) {
for (const match of matches) {
const response = this.parse(match, request);
responses.push(response);
}
}
else if (cleanResponse.length > 0) {
// Single response
responses.push(this.parse(cleanResponse, request));
}
return responses;
}
/**
* Parses a multi-line response (for multi-frame messages like VIN)
* Removes PCI (Protocol Control Information) bytes from each frame:
* - First Frame: byte 0 = PCI (0x0N where N = length)
* - Consecutive Frames: byte 0 = PCI (0x2N where N = sequence number)
*/
static parseMultiFrame(rawResponses, request) {
// Process each frame, removing PCI byte
const payloadBytes = [];
for (let i = 0; i < rawResponses.length; i++) {
const frame = rawResponses[i].replace(/[\r\n>]/g, '').trim();
if (frame.length === 0)
continue;
const bytes = frame.split(/\s+/).filter((b) => b.length > 0);
if (i === 0) {
// First Frame: Skip PCI byte (first byte indicates length)
// Format: [PCI] [Mode+0x40] [PID] [Data...]
payloadBytes.push(...bytes.slice(1));
}
else {
// Consecutive Frame: Skip PCI byte (0x2N where N = sequence)
// Format: [PCI] [Data...]
payloadBytes.push(...bytes.slice(1));
}
}
const combined = payloadBytes.join('');
return this.parse(combined, request);
}
}
exports.DiagnosticResponseParser = DiagnosticResponseParser;
//# sourceMappingURL=diagnostic-request.js.map