UNPKG

elm327

Version:

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

420 lines 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OBD2_COMMANDS = void 0; exports.getCommandByPid = getCommandByPid; exports.getAllCommands = getAllCommands; exports.getCommandsByCategory = getCommandsByCategory; /** * Converts a hexadecimal string to its decimal equivalent. */ const hexToDec = (hex) => parseInt(hex, 16); /** * Extracts data bytes from an OBD2 response, skipping mode + PID prefix. * Example: "410C1AF8" → mode=41, pid=0C, data=["1A","F8"] */ function extractDataBytes(response, commandPid) { const mode = commandPid.substring(0, 2); const pid = commandPid.substring(2).toUpperCase(); const expectedPrefix = (parseInt(mode, 16) + 0x40).toString(16).toUpperCase() + pid; const data = response.toUpperCase().replace(/\s+/g, ''); const idx = data.indexOf(expectedPrefix); const relevant = idx >= 0 ? data.substring(idx + expectedPrefix.length) : data; const bytes = []; for (let i = 0; i + 1 < relevant.length; i += 2) { const byte = relevant.substring(i, i + 2); if (/^[0-9A-F]{2}$/.test(byte)) bytes.push(byte); } return bytes; } /** * All predefined OBD2 commands with their PIDs and decoder functions. */ exports.OBD2_COMMANDS = { PIDS_00: { name: 'PIDS_00', pid: '0100', description: 'Supported PIDs (00-20)', decoder: (data) => { const clean = data.replace(/[\s\r\n>]/g, '').toUpperCase(); // Skip mode+pid prefix (4100) and extract 4 bytes (8 chars) const idx = clean.indexOf('4100'); const hex = idx >= 0 ? clean.substring(idx + 4, idx + 12) : clean.substring(0, 8); if (hex.length < 8) return []; const supported = []; for (let i = 0; i < 4; i++) { const byte = parseInt(hex.substring(i * 2, i * 2 + 2), 16); for (let bit = 0; bit < 8; bit++) { if ((byte & (1 << (7 - bit))) !== 0) { const pidNum = i * 8 + bit + 1; supported.push(pidNum.toString(16).toUpperCase().padStart(2, '0')); } } } return supported; }, unit: 'PID', }, DTC_STATUS: { name: 'DTC_STATUS', pid: '0101', description: 'DTC status since last clearing', decoder: (data) => { const clean = data.replace(/[\s\r\n>]/g, '').toUpperCase(); const idx = clean.indexOf('4101'); const hex = idx >= 0 ? clean.substring(idx + 4) : clean; const bytes = hex.match(/.{1,2}/g) || []; const statusByte = bytes.length > 0 ? parseInt(bytes[0], 16) : 0; const dtcCount = bytes.length > 1 ? parseInt(bytes[1], 16) : 0; return { milOn: (statusByte & 0x80) !== 0, dtcCount, readinessFlags: { misfire: (statusByte & 0x01) === 0, fuelSystem: (statusByte & 0x02) === 0, components: (statusByte & 0x04) === 0, }, }; }, unit: 'STATUS', }, ENGINE_LOAD: { name: 'ENGINE_LOAD', pid: '0104', description: 'Calculated engine load', decoder: (data) => { const bytes = extractDataBytes(data, '0104'); return bytes.length >= 1 ? (hexToDec(bytes[0]) * 100) / 255 : 0; }, unit: '%', }, COOLANT_TEMP: { name: 'COOLANT_TEMP', pid: '0105', description: 'Engine coolant temperature', decoder: (data) => { const bytes = extractDataBytes(data, '0105'); return bytes.length >= 1 ? hexToDec(bytes[0]) - 40 : 0; }, unit: '°C', }, FUEL_PRESSURE: { name: 'FUEL_PRESSURE', pid: '010A', description: 'Fuel pressure', decoder: (data) => { const bytes = extractDataBytes(data, '010A'); return bytes.length >= 1 ? hexToDec(bytes[0]) * 3 : 0; }, unit: 'kPa', }, INTAKE_PRESSURE: { name: 'INTAKE_PRESSURE', pid: '010B', description: 'Intake manifold absolute pressure (MAP)', decoder: (data) => { const bytes = extractDataBytes(data, '010B'); return bytes.length >= 1 ? hexToDec(bytes[0]) : 0; }, unit: 'kPa', }, ENGINE_RPM: { name: 'ENGINE_RPM', pid: '010C', description: 'Engine RPM', decoder: (data) => { const bytes = extractDataBytes(data, '010C'); if (bytes.length >= 2) { return (hexToDec(bytes[0]) * 256 + hexToDec(bytes[1])) / 4; } return 0; }, unit: 'rpm', }, VEHICLE_SPEED: { name: 'VEHICLE_SPEED', pid: '010D', description: 'Vehicle speed', decoder: (data) => { const bytes = extractDataBytes(data, '010D'); return bytes.length >= 1 ? hexToDec(bytes[0]) : 0; }, unit: 'km/h', }, TIMING_ADVANCE: { name: 'TIMING_ADVANCE', pid: '010E', description: 'Timing advance (cylinder 1)', decoder: (data) => { const bytes = extractDataBytes(data, '010E'); return bytes.length >= 1 ? hexToDec(bytes[0]) / 2 - 64 : 0; }, unit: '°', }, INTAKE_TEMP: { name: 'INTAKE_TEMP', pid: '010F', description: 'Intake air temperature', decoder: (data) => { const bytes = extractDataBytes(data, '010F'); return bytes.length >= 1 ? hexToDec(bytes[0]) - 40 : 0; }, unit: '°C', }, MAF_RATE: { name: 'MAF_RATE', pid: '0110', description: 'Mass air flow sensor air flow rate (MAF)', decoder: (data) => { const bytes = extractDataBytes(data, '0110'); if (bytes.length >= 2) { return (hexToDec(bytes[0]) * 256 + hexToDec(bytes[1])) / 100; } return 0; }, unit: 'g/s', }, THROTTLE_POS: { name: 'THROTTLE_POS', pid: '0111', description: 'Absolute throttle position', decoder: (data) => { const bytes = extractDataBytes(data, '0111'); return bytes.length >= 1 ? (hexToDec(bytes[0]) * 100) / 255 : 0; }, unit: '%', }, OBD_STANDARDS: { name: 'OBD_STANDARDS', pid: '011C', description: 'OBD standards compliance', decoder: (data) => { const bytes = extractDataBytes(data, '011C'); if (bytes.length === 0) return 'Unknown'; const value = parseInt(bytes[0], 16); // PID 011C returns a single byte value (1=OBD-II CARB, 2=OBD EPA, etc.) // NOT a bitmask! const map = { 1: 'OBD-II (CARB)', 2: 'OBD (EPA)', 3: 'OBD + OBD-II', 4: 'OBD-I', 5: 'Not OBD compliant', 6: 'EOBD', 7: 'EOBD + OBD-II', 9: 'OBD + EOBD', 10: 'JOBD', 11: 'JOBD + OBD-II', 12: 'JOBD + EOBD', 13: 'JOBD + OBD-II + EOBD', }; return map[value] || `Unknown (${value})`; }, }, // Oxygen (Lambda) Sensors - Mode 05 (O2 Test Results) O2S1_WR: { name: 'O2S1_WR', pid: '0113', description: 'O2 Sensor 1 Wide Range Equivalent Ratio', decoder: (data) => { const bytes = extractDataBytes(data, '0113'); if (bytes.length >= 2) { const value = (parseInt(bytes[0], 16) * 256 + parseInt(bytes[1], 16)) / 32768; return value.toFixed(2); } return 0; }, unit: 'λ', }, O2S2_WR: { name: 'O2S2_WR', pid: '0114', description: 'O2 Sensor 2 Wide Range Equivalent Ratio', decoder: (data) => { const bytes = extractDataBytes(data, '0114'); if (bytes.length >= 2) { const value = (parseInt(bytes[0], 16) * 256 + parseInt(bytes[1], 16)) / 32768; return value.toFixed(2); } return 0; }, unit: 'λ', }, O2S3_WR: { name: 'O2S3_WR', pid: '0115', description: 'O2 Sensor 3 Wide Range Equivalent Ratio', decoder: (data) => { const bytes = extractDataBytes(data, '0115'); if (bytes.length >= 2) { const value = (parseInt(bytes[0], 16) * 256 + parseInt(bytes[1], 16)) / 32768; return value.toFixed(2); } return 0; }, unit: 'λ', }, O2S4_WR: { name: 'O2S4_WR', pid: '0116', description: 'O2 Sensor 4 Wide Range Equivalent Ratio', decoder: (data) => { const bytes = extractDataBytes(data, '0116'); if (bytes.length >= 2) { const value = (parseInt(bytes[0], 16) * 256 + parseInt(bytes[1], 16)) / 32768; return value.toFixed(2); } return 0; }, unit: 'λ', }, O2S1_V: { name: 'O2S1_V', pid: '0117', description: 'O2 Sensor 1 Voltage', decoder: (data) => { const bytes = extractDataBytes(data, '0117'); return bytes.length >= 1 ? parseInt(bytes[0], 16) * 0.005 : 0; }, unit: 'V', }, O2S2_V: { name: 'O2S2_V', pid: '0118', description: 'O2 Sensor 2 Voltage', decoder: (data) => { const bytes = extractDataBytes(data, '0118'); return bytes.length >= 1 ? parseInt(bytes[0], 16) * 0.005 : 0; }, unit: 'V', }, O2S3_V: { name: 'O2S3_V', pid: '0119', description: 'O2 Sensor 3 Voltage', decoder: (data) => { const bytes = extractDataBytes(data, '0119'); return bytes.length >= 1 ? parseInt(bytes[0], 16) * 0.005 : 0; }, unit: 'V', }, O2S4_V: { name: 'O2S4_V', pid: '011A', description: 'O2 Sensor 4 Voltage', decoder: (data) => { const bytes = extractDataBytes(data, '011A'); return bytes.length >= 1 ? parseInt(bytes[0], 16) * 0.005 : 0; }, unit: 'V', }, O2S1_ST: { name: 'O2S1_ST', pid: '011B', description: 'O2 Sensor 1 Short Term Fuel Trim', decoder: (data) => { const bytes = extractDataBytes(data, '011B'); return bytes.length >= 1 ? ((parseInt(bytes[0], 16) - 128) * 100) / 128 : 0; }, unit: '%', }, RUNTIME: { name: 'RUNTIME', pid: '011F', description: 'Run time since engine start', decoder: (data) => { const bytes = extractDataBytes(data, '011F'); if (bytes.length >= 2) { return hexToDec(bytes[0]) * 256 + hexToDec(bytes[1]); } return 0; }, unit: 'seconds', }, FUEL_LEVEL: { name: 'FUEL_LEVEL', pid: '012F', description: 'Fuel tank level input', decoder: (data) => { const bytes = extractDataBytes(data, '012F'); return bytes.length >= 1 ? (hexToDec(bytes[0]) * 100) / 255 : 0; }, unit: '%', }, BAROMETRIC_PRESSURE: { name: 'BAROMETRIC_PRESSURE', pid: '0133', description: 'Absolute barometric pressure', decoder: (data) => { const bytes = extractDataBytes(data, '0133'); return bytes.length >= 1 ? hexToDec(bytes[0]) : 0; }, unit: 'kPa', }, AMBIENT_TEMP: { name: 'AMBIENT_TEMP', pid: '0146', description: 'Ambient air temperature', decoder: (data) => { const bytes = extractDataBytes(data, '0146'); return bytes.length >= 1 ? hexToDec(bytes[0]) - 40 : 0; }, unit: '°C', }, // Note: VIN uses ISO-TP multiframe — not fully supported on cheap clones. // Best handled with proper frame reassembly. VIN: { name: 'VIN', pid: '0902', description: 'Vehicle Identification Number (multiframe — limited support on clones)', decoder: (data) => { const cleaned = data.toUpperCase().replace(/4902[\dA-F]{2}/g, ''); const bytes = []; for (let i = 0; i + 1 < cleaned.length; i += 2) { const b = cleaned.substring(i, i + 2); if (/^[0-9A-F]{2}$/.test(b)) bytes.push(b); } const vin = bytes .map((b) => String.fromCharCode(hexToDec(b))) .join('') .replace(/[^\x20-\x7E]/g, '') .trim(); return vin || 'VIN not available'; }, unit: 'STRING', }, }; /** * Looks up an OBD2 command by its PID string. */ function getCommandByPid(pid) { return Object.values(exports.OBD2_COMMANDS).find((cmd) => cmd.pid.toUpperCase() === pid.toUpperCase()); } /** * Returns all predefined OBD2 commands. * Returns copies to prevent accidental mutation. */ function getAllCommands() { return Object.values(exports.OBD2_COMMANDS).map((cmd) => ({ ...cmd })); } /** * Returns commands filtered by category (MODE_01 or MODE_09). * Returns copies to prevent accidental mutation. */ function getCommandsByCategory(category) { if (category === 'MODE_01') { return Object.values(exports.OBD2_COMMANDS) .filter((cmd) => cmd.pid.startsWith('01')) .map((cmd) => ({ ...cmd })); } if (category === 'MODE_09') { return Object.values(exports.OBD2_COMMANDS) .filter((cmd) => cmd.pid.startsWith('09')) .map((cmd) => ({ ...cmd })); } return []; } //# sourceMappingURL=commands.js.map