├── example ├── .gitignore ├── poll-fake.ts ├── poll-rpm.js └── poll-rpm.ts ├── circle.yml ├── .gitignore ├── .npmignore ├── tslint.json ├── .editorconfig ├── ISSUE_TEMPLATE.md ├── lib ├── pids │ ├── conversions.ts │ ├── index.ts │ ├── data │ │ └── obd-spec-list.json │ └── pid.ts ├── constants.ts ├── interfaces.ts ├── pids.ts ├── log.ts ├── obd-interface.ts ├── connection.ts ├── parser.ts └── poller.ts ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /example/.gitignore: -------------------------------------------------------------------------------- 1 | !poll-rpm.js -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | NODE_PATH: . 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | coverage 5 | *.js 6 | .vscode 7 | typings 8 | *.d.ts 9 | *.js 10 | *.rdb -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .DS_Store 4 | 5 | tsconfig.json 6 | tslint.json 7 | typings.json 8 | typings 9 | *.ts 10 | !*.d.ts 11 | !*.js -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "quotemark": [true, "single", "avoid-escape"], 5 | "trailing-comma": [true, {"multiline": "never", "singleline": "never"}], 6 | "no-string-literal": false 7 | } 8 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Tells the .editorconfg plugin to stop searching once it finds this file 2 | root = true 3 | 4 | [*.ts] 5 | indent_size = 2 6 | indent_style = space 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.py] 16 | indent_size = 4 -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please fill in the information below too make sure it's easy to assist with issues :smile: 2 | 3 | ## Module Version Used 4 | `` 5 | 6 | ## Type of Connection 7 | [] Development (obd-parser-development-connection) 8 | [] Serial (obd-parser-serial-connection) 9 | [] Bluetooth (obd-parser-bluetooth-connection) 10 | 11 | ## Brief Description of Issue 12 | `` 13 | -------------------------------------------------------------------------------- /lib/pids/conversions.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Parses a hexadecimal string to regular base 10 6 | * @param {String} byte 7 | * @return {Number} 8 | */ 9 | export function parseHexToDecimal (byte: string) { 10 | return parseInt(byte, 16); 11 | }; 12 | 13 | 14 | /** 15 | * Converts an OBD value to a percentage 16 | * @param {String} byte 17 | * @return {Number} 18 | */ 19 | export function percentage (byte: string) { 20 | return parseHexToDecimal(byte) * (100 / 255); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export class OBD_MESSAGE_TYPES { 4 | static CURRENT_DATA = '01'; 5 | static REQUEST_DTC = '03'; 6 | static CLEAR_DTC = '04'; 7 | static VIN = '09'; 8 | }; 9 | 10 | export class OBD_OUTPUT_MESSAGE_TYPES { 11 | static MODE_01 = '41'; 12 | }; 13 | 14 | // We know a received message is complete when this character is received 15 | export const OBD_OUTPUT_DELIMETER: string = '>'; 16 | 17 | // This is the end of line delimeter used for OBD data. 18 | // We use it to terminate message strings being sent 19 | export const OBD_OUTPUT_EOL: string = '\r'; 20 | -------------------------------------------------------------------------------- /lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { PID } from './pids/pid'; 2 | 3 | export interface OBDConnection { 4 | write: Function, 5 | on: Function 6 | } 7 | 8 | export interface OBDOutput { 9 | ts: Date 10 | value: any|string|null 11 | pretty: string|null 12 | bytes: string 13 | name?: string 14 | pid?: string 15 | } 16 | 17 | export interface PIDArgs { 18 | pid: string, 19 | mode: string, 20 | bytes: number, 21 | name: string, 22 | min?: number, 23 | max?: number, 24 | unit: string 25 | } 26 | 27 | export interface PIDInfo { 28 | name: string, 29 | pid: string 30 | } 31 | 32 | 33 | export interface PollerArgs { 34 | pid: PID, 35 | interval: number|null 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "jsx": "react", 8 | "experimentalDecorators": false, 9 | "emitDecoratorMetadata": false, 10 | "declaration": true, 11 | "noImplicitAny": true, 12 | "removeComments": true, 13 | "noLib": false, 14 | "preserveConstEnums": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "sourceMap": false, 17 | "inlineSourceMap": true, 18 | "noImplicitReturns": true, 19 | "strictNullChecks": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "*": ["*"] 23 | } 24 | }, 25 | "compileOnSave": true 26 | } -------------------------------------------------------------------------------- /lib/pids.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as PIDS from './pids/pid'; 3 | import { OBD_MESSAGE_TYPES } from './constants'; 4 | 5 | export = { 6 | FuelLevel: new PIDS.FuelLevel(), 7 | Rpm: new PIDS.Rpm(), 8 | CoolantTemp: new PIDS.CoolantTemp(), 9 | VehicleSpeed: new PIDS.VehicleSpeed(), 10 | CalculatedEngineLoad: new PIDS.CalculatedEngineLoad(), 11 | FuelPressure: new PIDS.FuelPressure(), 12 | IntakeManifoldAbsolutePressure: new PIDS.IntakeManifoldAbsolutePressure(), 13 | IntakeAirTemperature: new PIDS.IntakeAirTemperature(), 14 | MafAirFlowRate: new PIDS.MafAirFlowRate(), 15 | ThrottlePosition: new PIDS.ThrottlePosition(), 16 | ObdStandard: new PIDS.ObdStandard(), 17 | FuelSystemStatus: new PIDS.FuelSystemStatus(), 18 | SupportedPids: new PIDS.SupportedPids() 19 | }; 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | A JavaScript library for interacting with the OBD (On Board Diagnostics) of 2 | vehicles equipped with such technology. 3 | 4 | Copyright (C) 2016 Evan Shortiss 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . -------------------------------------------------------------------------------- /lib/log.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as debug from 'debug'; 3 | 4 | const pkg = require('../package.json'); 5 | const defaultLogger = debug(pkg.name); 6 | 7 | export default function (name?: string) { 8 | if (name) { 9 | const log = debug(`${pkg.name} (${name})`); 10 | log('ok') 11 | return function (formatter: any, ...argsarr: any[]) { 12 | const args = Array.prototype.slice.call(arguments); 13 | 14 | args[0] = `${new Date().toISOString()} - ${args[0]}`; 15 | 16 | log.apply(log, args); 17 | }; 18 | } else { 19 | const log = debug(`${pkg.name}`); 20 | return function (formatter: any, ...argsarr: any[]) { 21 | const args = Array.prototype.slice.call(arguments); 22 | 23 | args[0] = `${new Date().toISOString()} - ${args[0]}`; 24 | 25 | log.apply(log, args); 26 | }; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lib/pids/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { map, find, keys } from 'ramda'; 4 | import { PID } from './pid'; 5 | import PIDS = require('../pids'); 6 | import { PIDInfo } from '../interfaces'; 7 | 8 | /** 9 | * Allows us to get a PID instance by matching an output hex code to 10 | * the code stored in the PID class. 11 | */ 12 | export function getPidByPidCode (pidstring: string) : PID|null { 13 | let names:Array = keys(PIDS); 14 | 15 | let pidname:string = find((name:string) => { 16 | let curpid:PID = PIDS[name]; 17 | 18 | return curpid.getPid() === pidstring; 19 | })(names); 20 | 21 | if (pidname) { 22 | return PIDS[pidname]; 23 | } else { 24 | return null; 25 | } 26 | }; 27 | 28 | 29 | /** 30 | * Returns a list that describes the supported PIDs. 31 | * List includes the PID code and name. 32 | */ 33 | export function getSupportedPidInfo () { 34 | return map((p: PID) => { 35 | let ret: PIDInfo = { 36 | name: p.getName(), 37 | pid: p.getPid() 38 | }; 39 | 40 | return ret; 41 | })(PIDS); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/obd-interface.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as connection from './connection'; 4 | import * as Promise from 'bluebird'; 5 | import * as PIDS from './pids/pid'; 6 | import generateLogger from './log'; 7 | 8 | const log = generateLogger(); 9 | 10 | // Export all PIDS classes so they can be passed to an ECUPoller 11 | export { PIDS as PIDS }; 12 | 13 | // Just export the vanilla ECUPoller class 14 | export { ECUPoller as ECUPoller } from './poller'; 15 | 16 | // Interfaces 17 | export { OBDOutput, PIDInfo, OBDConnection } from './interfaces'; 18 | 19 | 20 | /** 21 | * Initialises this module for usage (no shit - right?) 22 | * @param {Object} opts 23 | * @return {Promise} 24 | */ 25 | export function init (connectorFn: Function) : Promise { 26 | log('initialising obd-parser'); 27 | 28 | // Expose the connection we've been passed 29 | connection.setConnectorFn(connectorFn); 30 | 31 | // Call this to get a connection error/success now rather than later 32 | return connection.getConnection() 33 | .then(onInitialiseSuccess); 34 | 35 | 36 | function onInitialiseSuccess () { 37 | log('initialised successfully'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /lib/pids/data/obd-spec-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": "OBD-II as defined by the California Air Resources Board (CARB)", 3 | "2": "OBD as defined by the US Environmental Protection Agency (EPA)", 4 | "3": "OBD and OBD-II", 5 | "4": "OBD-I", 6 | "5": "Not OBD compliant", 7 | "6": "EOBD (Europe)", 8 | "7": "EOBD and OBD-II", 9 | "8": "EOBD and OBD", 10 | "9": "EOBD, OBD and OBD II", 11 | "10": "JOBD (Japan)", 12 | "11": "JOBD and OBD II", 13 | "12": "JOBD and EOBD", 14 | "13": "JOBD, EOBD, and OBD II", 15 | "17": "Engine Manufacturer Diagnostics (EMD)", 16 | "18": "Engine Manufacturer Diagnostics Enhanced (EMD+)", 17 | "19": "Heavy Duty On-Board Diagnostics (Child/Partial) (HD OBD-C)", 18 | "20": "Heavy Duty On-Board Diagnostics (HD OBD)", 19 | "21": "World Wide Harmonized OBD (WWH OBD)", 20 | "23": "Heavy Duty Euro OBD Stage I without NOx control (HD EOBD-I)", 21 | "24": "Heavy Duty Euro OBD Stage I with NOx control (HD EOBD-I N)", 22 | "25": "Heavy Duty Euro OBD Stage II without NOx control (HD EOBD-II)", 23 | "26": "Heavy Duty Euro OBD Stage II with NOx control (HD EOBD-II N)", 24 | "28": "Brazil OBD Phase 1 (OBDBr-1)", 25 | "29": "Brazil OBD Phase 2 (OBDBr-2)", 26 | "30": "Korean OBD (KOBD)", 27 | "31": "India OBD I (IOBD I)", 28 | "32": "India OBD II (IOBD II)", 29 | "33": "Heavy Duty Euro OBD Stage VI (HD EOBD-IV)" 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obd-parser", 3 | "version": "0.3.0", 4 | "description": "OBD interface that can send and receive messages over a given connection.", 5 | "main": "lib/obd-interface.js", 6 | "typings": "lib/obd-interface.d.ts", 7 | "dependencies": { 8 | "bluebird": "3.2.2", 9 | "case": "1.4.2", 10 | "debug": "2.3.3", 11 | "ramda": "0.22.1", 12 | "verror": "1.6.1" 13 | }, 14 | "devDependencies": { 15 | "@types/bluebird": "3.0.35", 16 | "@types/core-js": "0.9.35", 17 | "@types/debug": "0.0.29", 18 | "@types/node": "6.0.46", 19 | "@types/ramda": "0.0.2", 20 | "@types/verror": "1.6.28", 21 | "istanbul": "0.4.2", 22 | "linelint": "1.0.1", 23 | "obd-parser-development-connection": "~0.2.0", 24 | "obd-parser-serial-connection": "~0.1.1", 25 | "typescript": "2.0.10" 26 | }, 27 | "scripts": { 28 | "example": "npm run compile && tsc example/poll-rpm.ts && node example/poll-rpm.js", 29 | "example-fake": "npm run compile && tsc example/poll-fake.ts && node example/poll-fake.js", 30 | "compile": "tsc lib/obd-interface.ts --declaration", 31 | "prepublish": "npm run compile", 32 | "linelint": "linelint $(find lib/ -name \"*.ts\" -not -name \"*.d.ts\")", 33 | "test": "echo \"Tests needed...\" && npm run-script linelint" 34 | }, 35 | "keywords": [ 36 | "obd", 37 | "parser", 38 | "message", 39 | "obdii", 40 | "obd2", 41 | "elm", 42 | "elm327", 43 | "on board diagnostic" 44 | ], 45 | "author": "Evan Shortiss", 46 | "license": "GPL-3.0", 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/evanshortiss/obd-parser.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/evanshortiss/obd-parser/issues" 53 | }, 54 | "homepage": "https://github.com/evanshortiss/obd-parser" 55 | } 56 | -------------------------------------------------------------------------------- /example/poll-fake.ts: -------------------------------------------------------------------------------- 1 | 2 | // In your code this should be changed to 'obd-parser' 3 | import * as OBD from '../lib/obd-interface'; 4 | 5 | // Use a serial connection to connect 6 | var getConnector = require('obd-parser-development-connection'); 7 | 8 | // Returns a function that will allow us to connect to the serial port 9 | var connect:Function = getConnector({}); 10 | 11 | // Need to initialise the OBD module with a "connector" before starting 12 | OBD.init(connect) 13 | .then(function () { 14 | // We've successfully connected. Can now create ECUPoller instances 15 | const rpmPoller:OBD.ECUPoller = new OBD.ECUPoller({ 16 | // Pass an instance of the RPM PID to poll for RPM 17 | pid: new OBD.PIDS.FuelLevel(), 18 | // Poll every 1500 milliseconds 19 | interval: 1500 20 | }); 21 | 22 | const speedPoller:OBD.ECUPoller = new OBD.ECUPoller({ 23 | pid: new OBD.PIDS.VehicleSpeed(), 24 | interval: 1000 25 | }); 26 | 27 | // Bind an event handler for anytime RPM data is available 28 | rpmPoller.on('data', function (output: OBD.OBDOutput) { 29 | console.log('\n==== Got FuelLevel Output ===='); 30 | // Timestamp (Date object) for wheb the response was received 31 | console.log('time: ', output.ts); 32 | // The bytes returned from the ECU when asked from FuelLevel 33 | console.log('bytes: ', output.bytes); 34 | // A value that's usuall numeric e.g 1200 35 | console.log('value: ', output.value); 36 | // This will be a value such as "1200rpm" 37 | console.log('pretty: ', output.pretty); 38 | }); 39 | 40 | // Start polling (every 1500ms as specified above) 41 | rpmPoller.startPolling(); 42 | 43 | // Do a one time poll for speed 44 | speedPoller.poll() 45 | .then(function (output:OBD.OBDOutput) { 46 | console.log('\n==== Got Speed Output ===='); 47 | console.log('time: ', output.ts); 48 | console.log('bytes: ', output.bytes); 49 | console.log('value: ', output.value); 50 | console.log('pretty: ', output.pretty); 51 | }) 52 | .catch(function (err) { 53 | console.error('failed to poll the ECU', err); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /example/poll-rpm.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // In your code this should be changed to 'obd-parser' 3 | var OBD = require('../lib/obd-interface'); 4 | // Use a serial connection to connect 5 | var getConnector = require('obd-parser-serial-connection'); 6 | // Returns a function that will allow us to connect to the serial port 7 | var connect = getConnector({ 8 | // This might vary based on OS - this is the Mac OSX example 9 | serialPath: '/dev/tty.usbserial', 10 | // Might vary based on vehicle. This is the baudrate for a MK6 VW GTI 11 | serialOpts: { 12 | baudrate: 38400 13 | } 14 | }); 15 | // Need to initialise the OBD module with a "connector" before starting 16 | OBD.init(connect) 17 | .then(function () { 18 | // We've successfully connected. Can now create ECUPoller instances 19 | var rpmPoller = new OBD.ECUPoller({ 20 | // Pass an instance of the RPM PID to poll for RPM 21 | pid: new OBD.PIDS.Rpm(), 22 | // Poll every 1500 milliseconds 23 | interval: 1500 24 | }); 25 | var speedPoller = new OBD.ECUPoller({ 26 | pid: new OBD.PIDS.VehicleSpeed(), 27 | interval: 1000 28 | }); 29 | // Bind an event handler for anytime RPM data is available 30 | rpmPoller.on('data', function (output) { 31 | console.log('\n==== Got RPM Output ===='); 32 | // Timestamp (Date object) for wheb the response was received 33 | console.log('time: ', output.ts); 34 | // The bytes returned from the ECU when asked from RPM 35 | console.log('bytes: ', output.bytes); 36 | // A value that's usuall numeric e.g 1200 37 | console.log('value: ', output.value); 38 | // This will be a value such as "1200rpm" 39 | console.log('pretty: ', output.pretty); 40 | }); 41 | // Start polling (every 1500ms as specified above) 42 | rpmPoller.startPolling(); 43 | // Do a one time poll for speed 44 | speedPoller.poll() 45 | .then(function (output) { 46 | console.log('\n==== Got Speed Output ===='); 47 | console.log('time: ', output.ts); 48 | console.log('bytes: ', output.bytes); 49 | console.log('value: ', output.value); 50 | console.log('pretty: ', output.pretty); 51 | }) 52 | .catch(function (err) { 53 | console.error('failed to poll the ECU', err); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /example/poll-rpm.ts: -------------------------------------------------------------------------------- 1 | 2 | // In your code this should be changed to 'obd-parser' 3 | import * as OBD from '../lib/obd-interface'; 4 | 5 | // Use a serial connection to connect 6 | var getConnector = require('obd-parser-serial-connection'); 7 | 8 | // Returns a function that will allow us to connect to the serial port 9 | var connect:Function = getConnector({ 10 | // This might vary based on OS - this is the Mac OSX example 11 | serialPath: '/dev/tty.usbserial', 12 | 13 | // Might vary based on vehicle. This is the baudrate for a MK6 VW GTI 14 | serialOpts: { 15 | baudrate: 38400 16 | } 17 | }); 18 | 19 | // Need to initialise the OBD module with a "connector" before starting 20 | OBD.init(connect) 21 | .then(function () { 22 | // We've successfully connected. Can now create ECUPoller instances 23 | const rpmPoller:OBD.ECUPoller = new OBD.ECUPoller({ 24 | // Pass an instance of the RPM PID to poll for RPM 25 | pid: new OBD.PIDS.Rpm(), 26 | // Poll every 1500 milliseconds 27 | interval: 1500 28 | }); 29 | 30 | const speedPoller:OBD.ECUPoller = new OBD.ECUPoller({ 31 | pid: new OBD.PIDS.VehicleSpeed(), 32 | interval: 1000 33 | }); 34 | 35 | // Bind an event handler for anytime RPM data is available 36 | rpmPoller.on('data', function (output: OBD.OBDOutput) { 37 | console.log('\n==== Got RPM Output ===='); 38 | // Timestamp (Date object) for wheb the response was received 39 | console.log('time: ', output.ts); 40 | // The bytes returned from the ECU when asked from RPM 41 | console.log('bytes: ', output.bytes); 42 | // A value that's usuall numeric e.g 1200 43 | console.log('value: ', output.value); 44 | // This will be a value such as "1200rpm" 45 | console.log('pretty: ', output.pretty); 46 | }); 47 | 48 | // Start polling (every 1500ms as specified above) 49 | rpmPoller.startPolling(); 50 | 51 | // Do a one time poll for speed 52 | speedPoller.poll() 53 | .then(function (output:OBD.OBDOutput) { 54 | console.log('\n==== Got Speed Output ===='); 55 | console.log('time: ', output.ts); 56 | console.log('bytes: ', output.bytes); 57 | console.log('value: ', output.value); 58 | console.log('pretty: ', output.pretty); 59 | }) 60 | .catch(function (err) { 61 | console.error('failed to poll the ECU', err); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /lib/connection.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as Promise from 'bluebird'; 4 | import { OBD_OUTPUT_EOL } from './constants'; 5 | import { getParser } from './parser'; 6 | import { OBDConnection } from './interfaces'; 7 | import generateLogger from './log'; 8 | 9 | const log = generateLogger('connection'); 10 | 11 | let connectorFn: Function; 12 | let connection:OBDConnection|null = null; 13 | 14 | let msgRecvCount = 0; 15 | let msgSendCount = 0; 16 | 17 | /** 18 | * Sets the connection to be used. This should be passed a 19 | * pre-configured connector 20 | */ 21 | export function setConnectorFn (cFn: Function) { 22 | log('setting connnection function'); 23 | connectorFn = cFn; 24 | }; 25 | 26 | 27 | /** 28 | * Returns a pre-configured connection instance. 29 | * @return {OBDConnection} 30 | */ 31 | export function getConnection () { 32 | if (!connectorFn) { 33 | throw new Error( 34 | 'cannot get connection. please ensure connectorFn was passed to init' 35 | ); 36 | } 37 | 38 | if (connection) { 39 | return Promise.resolve(connection); 40 | } 41 | 42 | log('getting connnection'); 43 | return connectorFn(configureConnection); 44 | }; 45 | 46 | 47 | /** 48 | * We need to configure the given connection with some sensible defaults 49 | * and also optimisations to ensure best data transfer rates. 50 | * 51 | * @param {OBDConnection} conn A connection object from this module's family 52 | */ 53 | export function configureConnection (conn: OBDConnection) { 54 | log('configuring obd connection'); 55 | 56 | connection = conn; 57 | 58 | // Need to ensure each line is terminated when written 59 | let write:Function = conn.write.bind(conn); 60 | let queue:Array = []; 61 | let locked:boolean = false; 62 | 63 | function doWrite (msg: string) { 64 | log(`writing "${msg}". queue state ${JSON.stringify(queue)}`); 65 | log(`send count ${msgSendCount}. receive count ${msgRecvCount}`); 66 | locked = true; 67 | 68 | // Need to write the number of expected replies for poller messages 69 | // TODO: Better implementation for passing expected replies count... 70 | if (msg.indexOf('AT') === -1) { 71 | // Generate the final message to be sent, e.g "010C1\r" (add the final '1') 72 | msg = msg + '1'; 73 | } 74 | 75 | msg = msg.concat(OBD_OUTPUT_EOL); 76 | 77 | log(`writing message ${msg}. connection will lock`); 78 | 79 | // When next "line-break" event is emitted by the parser we can send 80 | // next message since we know it has been processed - we don't care 81 | // about success etc 82 | getParser().once('line-break', function () { 83 | msgRecvCount++; 84 | 85 | // Get next queued message (FIFO ordering) 86 | let payload:string|undefined = queue.shift(); 87 | 88 | locked = false; 89 | 90 | log(`new line detected by parser. connection unlocked. queue contains ${JSON.stringify(queue)}`); 91 | 92 | if (payload) { 93 | log( 94 | 'writing previously queued payload "%s". queue now contains %s', 95 | payload, 96 | JSON.stringify(queue) 97 | ); 98 | // Write a new message (FIFO) 99 | conn.write(payload); 100 | } else { 101 | log('no payloads are queued'); 102 | } 103 | }); 104 | 105 | // Write the formatted message to the obd interface 106 | write(msg); 107 | } 108 | 109 | // Overwrite the public write function with our own 110 | conn.write = function _obdWrite (msg:string) { 111 | if (!locked && msg) { 112 | msgSendCount++; 113 | log(`queue is unlocked, writing message "${msg}"`); 114 | doWrite(msg); 115 | } else if (msg) { 116 | queue.push(msg); 117 | log( 118 | 'queue is locked. queued message %s. entries are %s', 119 | JSON.stringify(msg), 120 | queue.length, 121 | JSON.stringify(queue) 122 | ); 123 | } 124 | }; 125 | 126 | // Pipe all output from the serial connection to our parser 127 | conn.on('data', function (str) { 128 | log(`received data "${str}"`); 129 | getParser().write(str); 130 | }); 131 | 132 | // Configurations below are from node-serial-obd and python-OBD 133 | 134 | // No echo 135 | conn.write('ATE0'); 136 | // Remove linefeeds 137 | conn.write('ATL0'); 138 | // This disables spaces in in output, which is faster! 139 | conn.write('ATS0'); 140 | // Turns off headers and checksum to be sent. 141 | conn.write('ATH0'); 142 | // Turn adaptive timing to 2. This is an aggressive learn curve for adjusting 143 | // the timeout. Will make huge difference on slow systems. 144 | conn.write('ATAT2'); 145 | // Set timeout to 10 * 4 = 40msec, allows +20 queries per second. This is 146 | // the maximum wait-time. ATAT will decide if it should wait shorter or not. 147 | conn.write('ATST0A'); 148 | // Use this to set protocol automatically, python-OBD uses "ATSPA8", but 149 | // seems to have issues. Maybe this should be an option we can pass? 150 | conn.write('ATSP0'); 151 | 152 | // TODO: use events instead 153 | // Nasty way to make sure configuration calls have been performed before use 154 | return new Promise((resolve) => { 155 | let interval: NodeJS.Timer = setInterval(() => { 156 | if (queue.length === 0) { 157 | clearInterval(interval); 158 | setTimeout(function () { 159 | log('connection intialisation complete'); 160 | resolve(conn); 161 | }, 500); 162 | } else { 163 | log('connection initialising...'); 164 | } 165 | }, 250); 166 | }); 167 | } 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | obd-parser 2 | ========== 3 | 4 | [![Circle CI](https://circleci.com/gh/evanshortiss/obd-parser/tree/master.svg?style=svg)](https://circleci.com/gh/evanshortiss/obd-parser/tree/master) 5 | 6 | 7 | A module for interacting with the OBD (On Board Diagnostics) of vehicles 8 | via ELM 327 connections. 9 | 10 | ## Install 11 | 12 | ``` 13 | npm install obd-parser --save 14 | ``` 15 | 16 | After this you will need to install a module that facilitates connecting to 17 | the ECU. At present only _obd-parser-serial-connection_ is available - this 18 | requires a USB to OBD connection such as [these](https://www.amazon.com/s/ref=nb_sb_noss/163-4411256-8552763?url=search-alias%3Daps&field-keywords=obd+usb). 19 | 20 | ``` 21 | npm install obd-parser-serial-connection --save 22 | ``` 23 | 24 | ## Usage 25 | This example uses TypeScript, but the examples folder has the JavaScript code, 26 | the JavaScript code is also pasted below for reference. 27 | 28 | ```ts 29 | 30 | // In your code this should be changed to 'obd-parser' 31 | import * as OBD from 'obd-parser'; 32 | 33 | // Use a serial connection to connect 34 | var getConnector = require('obd-parser-serial-connection'); 35 | 36 | // Returns a function that will allow us to connect to the serial port 37 | var connectorFn:Function = getConnector({ 38 | // This might vary based on OS - this is the Mac OSX example 39 | serialPath: '/dev/tty.usbserial', 40 | 41 | // Might vary based on vehicle. This is the baudrate for a MK6 VW GTI 42 | serialOpts: { 43 | baudrate: 38400 44 | } 45 | }); 46 | 47 | // Need to initialise the OBD module with a "connector" before starting 48 | OBD.init(connectorFn) 49 | .then(function () { 50 | // We've successfully connected. Can now create ECUPoller instances 51 | const rpmPoller:OBD.ECUPoller = new OBD.ECUPoller({ 52 | // Pass an instance of the RPM PID to poll for RPM 53 | pid: new OBD.PIDS.Rpm(), 54 | // Poll every 1500 milliseconds 55 | interval: 1500 56 | }); 57 | 58 | // Bind an event handler for anytime RPM data is available 59 | rpmPoller.on('data', function (output: OBD.OBDOutput) { 60 | console.log('==== Got RPM Output ===='); 61 | // Timestamp (Date object) for wheb the response was received 62 | console.log('time: ', output.ts); 63 | // The bytes returned from the ECU when asked from RPM 64 | console.log('bytes: ', output.bytes); 65 | // A value that's usuall numeric e.g 1200 66 | console.log('value: ', output.value); 67 | // This will be a value such as "1200rpm" 68 | console.log('pretty: ', output.pretty); 69 | }); 70 | 71 | // Start polling (every 1500ms as specified above) 72 | rpmPoller.startPolling(); 73 | }); 74 | 75 | ``` 76 | 77 | 78 | ## Supported PIDs 79 | Currently the module has support for a limited number of PIDs. PID support can 80 | easily be added by adding a new PID definition in _lib/pids/pid.ts_. You can find 81 | information that will assist PID implementation on 82 | [this Wiki](https://en.wikipedia.org/wiki/OBD-II_PIDs). 83 | 84 | For the most up to date list see this 85 | [directory](https://github.com/evanshortiss/obd-reader/tree/master/lib/pids/pid.ts), 86 | or the below list: 87 | 88 | * ENGINE_COOLANT_TEMPERATURE (05) 89 | * FUEL_LEVEL_INPUT (2F) 90 | * ENGINE_RPM (0C) 91 | * VEHICLE_SPEED (0D) 92 | 93 | If using TypeScript you can also type "OBD.PIDS" and intellisense will display 94 | available options. 95 | 96 | 97 | ## API 98 | 99 | ### OBD.init(connectorFn) 100 | Initialise the parser. _connectorFn_ should be a connector function 101 | generated by a module such as obd-parser-serial-connection. 102 | 103 | ### OBD.ECUPoller(args: PollerArgs) (Class) 104 | A class that can be used to create ECUPoller instances. These must be passed 105 | an args Object that contains: 106 | 107 | * pid - An instance of any PID, e.g _new OBD.PIDS.Rpm()_ 108 | * interval - The number of milliseconds two wait bewteen polling if 109 | _startPolling()_ is called. 110 | 111 | You should only create one instance of a given ECUPoller and PID combination 112 | at a time unless you're sure about what you're doing. 113 | 114 | #### ECUPoller.poll() 115 | Sends a poll to the ECU for this ECUPoller's PID. Returns Promise that will 116 | resolve with an Object matching the OBDOutput interface. If you want you can 117 | ignore the Promise and instead bind a "data" listener like the example in this 118 | README file. 119 | 120 | #### ECUPoller.startPolling() and ECUPoller.stopPolling() 121 | Starts a constant poll loop that queries as near to the _args.interval_ passed 122 | to this ECUPoller. If it is not receiving responses it will not send new polls 123 | even if it reaches the interval since we want to prevent flooding the ECU. 124 | 125 | ### OBD.PIDS 126 | This is an Object with PID Classes attached, currently valid options are 127 | demonstrated below: 128 | 129 | ```ts 130 | import * as OBD from 'obd-parser'; 131 | 132 | new OBD.PIDS.FuelLevel(); 133 | new OBD.PIDS.Rpm(); 134 | new OBD.PIDS.VehicleSpeed(); 135 | new OBD.PIDS.CoolantTemp(); 136 | 137 | // This is the base class used by all above PIDS. You might want to extend 138 | // this to create your own PIDs (don't forget to contribute them here!) 139 | new OBD.PIDS.PID(); 140 | ``` 141 | 142 | ### OBD.OBDOutput 143 | Used in TypeScript environments to apply typing to poll "data" events and 144 | Promise results. 145 | 146 | 147 | ## Pure JavaScript Example 148 | 149 | Pure JavaScript example. It is almost identical to the TypeScript version from 150 | above in this README. 151 | 152 | ```javascript 153 | 'use strict'; 154 | 155 | var OBD = require('obd-parser'); 156 | 157 | var getConnector = require('obd-parser-serial-connection'); 158 | 159 | var connect = getConnector({ 160 | serialPath: '/dev/tty.usbserial', 161 | serialOpts: { 162 | baudrate: 38400 163 | } 164 | }); 165 | 166 | OBD.init(connect) 167 | .then(function () { 168 | var rpmPoller = new OBD.ECUPoller({ 169 | pid: new OBD.PIDS.Rpm(), 170 | interval: 1500 171 | }); 172 | 173 | rpmPoller.on('data', function (output) { 174 | console.log('==== Got RPM Output ===='); 175 | console.log('time: ', output.ts); 176 | console.log('bytes: ', output.bytes); 177 | console.log('value: ', output.value); 178 | console.log('pretty: ', output.pretty); 179 | }); 180 | 181 | rpmPoller.startPolling(); 182 | }); 183 | ``` 184 | 185 | # CHANGELOG 186 | 187 | * 0.2.1 188 | * Ensure definition files are included in published code 189 | 190 | * 0.2.0 191 | * Rewrite using TypeScript and Classes (inner conflict regarding Classes) 192 | * Simplify getting and creating ECUPollers and PIDS 193 | * Upadted documentation and example 194 | 195 | * < 0.2.0 - (ಠ_ಠ) -------------------------------------------------------------------------------- /lib/parser.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as R from 'ramda'; 4 | import { Transform } from 'stream'; 5 | import { OBD_OUTPUT_DELIMETER, OBD_OUTPUT_MESSAGE_TYPES } from './constants'; 6 | import VError = require('verror'); 7 | import * as Promise from 'bluebird'; 8 | import * as pids from './pids/index'; 9 | import { PID } from './pids/pid'; 10 | import generateLogger from './log'; 11 | import { OBDOutput } from './interfaces'; 12 | 13 | const log = generateLogger('OBDStreamParser'); 14 | 15 | let parser: OBDStreamParser; 16 | 17 | export class OBDStreamParser extends Transform { 18 | private _buffer: string; 19 | 20 | public constructor () { 21 | super(); 22 | 23 | this._buffer = ''; 24 | } 25 | 26 | public _flush (done: Function) { 27 | this._buffer = ''; 28 | done(); 29 | } 30 | 31 | public _transform (input: Buffer, encoding: String, done: Function): void { 32 | let data = input.toString('utf8'); 33 | let self = this; 34 | 35 | log('received data %s', data); 36 | 37 | // Remove any linebreaks from input, and add to buffer. We need the double 38 | // escaped replace due to some data having extra back ticks...wtf 39 | self._buffer += data; 40 | 41 | log('current buffer: %s', JSON.stringify(self._buffer)); 42 | 43 | if (hasPrompt(self._buffer)) { 44 | // We have a full output from the OBD interface e.g "410C1B56\r\r>" 45 | log('serial output completed. parsing'); 46 | 47 | // The hex lines from the current buffer 48 | let outputs: Array = extractOutputStrings(self._buffer); 49 | 50 | // Trigger a "data" event for each valid hex output received 51 | Promise.map(outputs, (o: string) => { 52 | return parseObdString(o) 53 | .then((parsed) => { 54 | if (parsed) { 55 | self.emit('data', parsed); 56 | } 57 | }) 58 | .catch((err) => { 59 | self.emit('error', err); 60 | }); 61 | }) 62 | .finally(() => { 63 | // Let listeners know that they can start to write again 64 | self.emit('line-break'); 65 | 66 | // Reset the buffer since we've successfully parsed it 67 | self._flush(done); 68 | }); 69 | } else { 70 | log('data was not a complete output'); 71 | done(); 72 | } 73 | return; 74 | } 75 | } 76 | 77 | 78 | /** 79 | * Determines if the passed buffer/string has a delimeter 80 | * that indicates it has been completed. 81 | * @param {String} data 82 | * @return {Boolean} 83 | */ 84 | function hasPrompt (data: string) { 85 | // Basically, we check that the a newline has started 86 | return data.indexOf(OBD_OUTPUT_DELIMETER) !== -1; 87 | } 88 | 89 | 90 | /** 91 | * Commands can be separated on multiple lines, we need each line separately 92 | * @param {String} buffer 93 | * @return {Array} 94 | */ 95 | function extractOutputStrings (buffer: string) { 96 | log( 97 | 'extracting command strings from buffer %s', 98 | JSON.stringify(buffer) 99 | ); 100 | 101 | // Extract multiple commands if they exist in the String by replacing 102 | // linebreaks and splitting on the newline delimeter 103 | // We replace double backticks. They only seem to occur in a test case 104 | // but we need to deal with it anyway, just in case... 105 | let cmds: Array = buffer 106 | .replace(/\n/g, '') 107 | .replace(/\\r/g, '\r') 108 | .split(/\r/g); 109 | 110 | // Remove the new prompt char 111 | cmds = R.map((c:string) => { 112 | return c 113 | .replace(OBD_OUTPUT_DELIMETER, '') 114 | .replace(/ /g, '') 115 | .trim(); 116 | })(cmds); 117 | 118 | // Remove empty commands 119 | cmds = R.filter((c: string) => { 120 | return !R.isEmpty(c); 121 | }, cmds); 122 | 123 | log( 124 | 'extracted strings %s from buffer %s', 125 | JSON.stringify(cmds), 126 | buffer 127 | ); 128 | 129 | return cmds; 130 | } 131 | 132 | 133 | /** 134 | * Determines if an OBD string is parseable by ensuring it's not a 135 | * generic message output 136 | * @param {String} str 137 | * @return {Boolean} 138 | */ 139 | function isHex (str: string) { 140 | return (str.match(/^[0-9A-F]+$/)) ? true : false; 141 | } 142 | 143 | 144 | /** 145 | * Convert the returned bytes into their pairs if possible, or return null 146 | * @param {String} str 147 | * @return {Array|null} 148 | */ 149 | function getByteGroupings (str: string) : Array|null { 150 | log('extracting byte groups from %s', JSON.stringify(str)); 151 | 152 | // Remove white space (if any exists) and get byte groups as pairs 153 | return str.replace(/\ /g, '').match(/.{1,2}/g); 154 | } 155 | 156 | 157 | /** 158 | * Parses an OBD output into useful data for developers 159 | * @param {String} str 160 | * @return {Object} 161 | */ 162 | function parseObdString (str: string) : Promise { 163 | log('parsing command string %s', str); 164 | 165 | let bytes = getByteGroupings(str); 166 | 167 | let ret:OBDOutput = { 168 | ts: new Date(), 169 | bytes: str, 170 | value: null, 171 | pretty: null 172 | }; 173 | 174 | if (!isHex(str)) { 175 | log( 176 | 'received generic (non hex) string output "%s", not parsing', 177 | str 178 | ); 179 | return Promise.resolve(ret); 180 | } else if (bytes && bytes[0] === OBD_OUTPUT_MESSAGE_TYPES.MODE_01) { 181 | log( 182 | 'received valid output "%s" of type "%s", parsing', 183 | str, 184 | OBD_OUTPUT_MESSAGE_TYPES.MODE_01 185 | ); 186 | 187 | let pidCode: string = bytes[1]; 188 | 189 | let pid:PID|null = pids.getPidByPidCode(pidCode); 190 | 191 | if (pid) { 192 | log(`we have a matching class for code "${pidCode}"`); 193 | // We have a class that knows how to deal with this pid output. Parse it! 194 | ret.pretty = pid.getFormattedValueForBytes(bytes); 195 | 196 | // pass all bytes returned and have the particular PID figure it out 197 | ret.value = pid.getValueForBytes(bytes.slice(0)); 198 | 199 | ret.name = pid.getName(); 200 | ret.pid = pid.getPid() 201 | 202 | return Promise.resolve(ret); 203 | } else { 204 | log('no match found for pid %s', pidCode); 205 | // Emit the data, but just the raw bytes 206 | return Promise.resolve(ret); 207 | } 208 | } else { 209 | // Wasn't a recognised message type - was probably our own bytes 210 | // since the serial module outputs those as "data" for some reason 211 | return Promise.resolve(null); 212 | } 213 | } 214 | 215 | 216 | /** 217 | * Parses realtime type OBD data to a useful format 218 | * @param {Array} byteGroups 219 | * @return {Mixed} 220 | */ 221 | function getValueForPidFromPayload (bytes: Array) : Promise { 222 | log('parsing a realtime command with bytes', bytes.join()); 223 | 224 | let pidType: string = bytes[1]; 225 | 226 | return Promise.resolve(bytes) 227 | .then((bytes) => pids.getPidByPidCode(pidType)) 228 | .then((pid) => { 229 | 230 | if (!pid) { 231 | // We don't have a class for this PID type so we can't handle it 232 | return Promise.reject( 233 | new VError( 234 | 'failed to find an implementation for PID "%s" in payload "%s"', 235 | pidType, 236 | bytes.join('') 237 | ) 238 | ); 239 | } 240 | 241 | // Depending on the payload type we only parse a certain number of bytes 242 | let bytesToParse: Array = bytes.slice( 243 | 2, 244 | pid.getParseableByteCount() 245 | ); 246 | 247 | // TODO: method overloading vs. apply? TS/JS (-_-) 248 | return pid.getValueForBytes.apply(pid, bytesToParse); 249 | }); 250 | } 251 | 252 | export function getParser (): OBDStreamParser { 253 | if (parser) { 254 | return parser; 255 | } else { 256 | return parser = new OBDStreamParser(); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /lib/poller.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { getConnection } from './connection'; 4 | import { EventEmitter } from 'events'; 5 | import { PollerArgs, OBDOutput, OBDConnection } from './interfaces'; 6 | import { getParser } from './parser'; 7 | import * as Promise from 'bluebird'; 8 | import{ IDebugger } from 'debug'; 9 | import generateLogger from './log'; 10 | 11 | 12 | /** 13 | * Constructor function to create a poller instance. 14 | * 15 | * Poller instances will request data from the ECU at a defined refresh rate. 16 | * 17 | * @param {Object} opts 18 | */ 19 | export class ECUPoller extends EventEmitter { 20 | 21 | private lastResponseTs: number|null; 22 | private lastPollTs: number|null; 23 | private pollTimer?: NodeJS.Timer; 24 | private polling: boolean; 25 | private args: PollerArgs; 26 | private locked: boolean; 27 | private msgSendCount: number = 0; 28 | private msgRecvCount: number = 0; 29 | private timeoutTimer?: NodeJS.Timer; 30 | private log: Function; 31 | private timeoutFn?: Function; 32 | private curListener?: Function; 33 | 34 | public constructor (args: PollerArgs) { 35 | super(); 36 | 37 | this.args = args; 38 | this.lastResponseTs = null; 39 | this.lastPollTs = null; 40 | this.polling = false; 41 | this.locked = false; 42 | this.log = generateLogger(args.pid.getName()); 43 | 44 | this.log('created poller for %s', args.pid.getName()); 45 | } 46 | 47 | /** 48 | * We want to get as close to the requested refresh rate as possible. 49 | * This means if the ECU has a response delay then we account for it. 50 | * 51 | * @param {Number} max The max delay in between pools 52 | * @param {Number} lastPollTs The time we issued the last poll 53 | * @return {Number} 54 | */ 55 | private getNextPollDelay () : number { 56 | if (this.lastPollTs) { 57 | // A poll has occurred previously. If we're calling this function 58 | // before the max interval time is reached then we must wait n ms 59 | // where n is the difference between the max poll rate and last poll sent 60 | const delta = this.lastResponseTs - this.lastPollTs; 61 | 62 | if (delta <= 0) { 63 | this.log(`delta between lastResponseTs and lastPollTs for ${this.args.pid.getName()} was ${delta}. defaulting to interval of ${this.args.interval || 1000}`); 64 | return this.args.interval || 1000; 65 | } 66 | 67 | const nextPoll = delta > this.args.interval ? 0 : this.args.interval - delta; 68 | 69 | this.log( 70 | 'getting poll time for %s, using last time of %s vs now %s. delta is %dms.', 71 | this.args.pid.getName(), 72 | new Date(this.lastPollTs).toISOString(), 73 | new Date().toISOString(), 74 | delta 75 | ); 76 | 77 | this.log(`next poll in ${nextPoll}ms`); 78 | 79 | return nextPoll; 80 | } else { 81 | // No previous poll has occurred yet so fire one right away 82 | return 0; 83 | } 84 | } 85 | 86 | /** 87 | * Locks this poller to prevent it sending any more messages to the ECU 88 | * @return {void} 89 | */ 90 | private lock () { 91 | this.locked = true; 92 | } 93 | 94 | /** 95 | * Unlocks this poller to allow it to send more messages to the ECU 96 | * @return {void} 97 | */ 98 | private unlock () { 99 | this.locked = false; 100 | } 101 | 102 | /** 103 | * Returns a boolean, where true indicates this instance is locked 104 | * @return {boolean} 105 | */ 106 | private isLocked (): boolean { 107 | return this.locked; 108 | } 109 | 110 | /** 111 | * Returns a boolean indicating if the provided OBDOutput is designated 112 | * for this Poller instance 113 | * @return {boolean} 114 | */ 115 | private isMatchingPayload (data: OBDOutput) { 116 | return data.bytes ? 117 | data.bytes.substr(2, 2) === this.args.pid.getPid() : false; 118 | } 119 | 120 | 121 | /** 122 | * Polls the ECU for this specifc ECUPoller's PID. Use this if you want to 123 | * poll on demand rather than on an interval. 124 | * 125 | * This method returns a Promise, but you can also bind a handler for the 126 | * "data" event if that is preferable. 127 | */ 128 | public poll (): Promise { 129 | const self = this; 130 | 131 | this.log('poll was called'); 132 | 133 | // Cannot call this function if we're already polling 134 | if (this.polling) { 135 | return Promise.reject( 136 | new Error(`${self.args.pid.getName()} - cannot call poll when polling loop is active`) 137 | ); 138 | } 139 | 140 | return new Promise ((resolve, reject) => { 141 | const bytesToWrite:string = self.args.pid.getWriteString(); 142 | 143 | self.log(`getting connection to poll (${self.args.pid.getPid()})`); 144 | 145 | const listener = (output: OBDOutput) => { 146 | self.log('poll received response. removing listener'); 147 | self.unsetTimeoutOperation(); 148 | self.removeOutputListener(); 149 | resolve(output); 150 | }; 151 | 152 | self.setTimeoutOperation(() => { 153 | reject(new Error(`polling timedout`)); 154 | }, 1000); 155 | 156 | self.addOutputListener(listener); 157 | 158 | self.writeBytesToEcu() 159 | .catch(reject); 160 | }); 161 | } 162 | 163 | /** 164 | * Starts this poller polling. This means it will poll at the interval 165 | * defined in the args, or as close as possible to that 166 | * @return {void} 167 | */ 168 | public startPolling () { 169 | const self = this; 170 | 171 | this.log('start poll interval for %s', this.args.pid.getName()); 172 | 173 | if (this.polling) { 174 | self.log('called startPolling, but it was already started'); 175 | return; 176 | } 177 | 178 | // Need to lock this component in polling state (TODO: use an FSM pattern) 179 | this.polling = true; 180 | 181 | // Function we can reuse to do the initial poll should it fail 182 | function doInitialPoll () { 183 | self.pollTimer = setTimeout(() => { 184 | self.log('sending initial poll for polling loop'); 185 | 186 | // Handle "data" events when emitted 187 | self.log('added data listener for poll loop'); 188 | self.addOutputListener(self.onPollLoopData.bind(self)); 189 | 190 | // If the initial poll dies not get data quickly enough we need to 191 | // take action by retrying until it succeeds 192 | self.setTimeoutOperation(() => { 193 | self.log('poll loop failed to get data for initial poll. retrying immediately'); 194 | doInitialPoll(); 195 | }, 5000); 196 | 197 | self.writeBytesToEcu() 198 | .catch((e: Error) => { 199 | self.log('error doing poll loop - ', e.stack); 200 | self.log('retrying in 1 second'); 201 | setTimeout(doInitialPoll, 1000) 202 | }); 203 | }, 250); 204 | } 205 | 206 | // Get started! 207 | doInitialPoll(); 208 | } 209 | 210 | private removeOutputListener () { 211 | if (this.curListener) { 212 | this.log('removing "data" listener from parser until new poll is sent'); 213 | getParser().removeListener('data', this.curListener); 214 | this.curListener = undefined; 215 | } 216 | } 217 | 218 | private addOutputListener (listener: Function) { 219 | if (this.curListener) { 220 | this.log('poller cannot add multiple listeners. removing cur listener'); 221 | this.removeOutputListener(); 222 | } 223 | 224 | this.curListener = listener; 225 | getParser().addListener('data', listener); 226 | } 227 | 228 | 229 | private writeBytesToEcu () { 230 | const self = this; 231 | 232 | return getConnection() 233 | .then((conn: OBDConnection) => { 234 | self.msgSendCount++; 235 | 236 | self.lastPollTs = Date.now(); 237 | 238 | const bytesToWrite:string = self.args.pid.getWriteString(); 239 | self.log(`got connection. writing data "${bytesToWrite}"`); 240 | 241 | conn.write(bytesToWrite); 242 | }); 243 | } 244 | 245 | private setTimeoutOperation (fn: Function, ts: number) { 246 | const self = this; 247 | 248 | // Clear an existing timeout event 249 | this.unsetTimeoutOperation(); 250 | 251 | self.log(`adding new timeout event with delay of ${ts}ms`); 252 | 253 | // If after 2500ms we haven't received data then we need to take an action 254 | this.timeoutTimer = setTimeout(() => { 255 | self.log('poll operation timed out. trigger supplied callback'); 256 | fn(); 257 | }, ts); 258 | 259 | // Generate a timeout function. We store it so it can be removed 260 | this.timeoutFn = (output: OBDOutput) => { 261 | if (self.timeoutTimer && self.isMatchingPayload(output)) { 262 | self.log('received relevant data event. removing timeout handler'); 263 | self.unsetTimeoutOperation(); 264 | } 265 | }; 266 | 267 | getParser().once('data', this.timeoutFn); 268 | } 269 | 270 | 271 | private unsetTimeoutOperation () { 272 | if (this.timeoutTimer) { 273 | if (this.timeoutFn) { 274 | getParser().removeListener('data', this.timeoutFn); 275 | } 276 | 277 | clearTimeout(this.timeoutTimer); 278 | this.timeoutTimer = undefined; 279 | this.timeoutFn = undefined; 280 | this.log('cleared existing poller timeout event'); 281 | } 282 | } 283 | 284 | 285 | /** 286 | * Called when we receive poller data if this.polling is true 287 | * @param {OBDOutput} output 288 | */ 289 | private onPollLoopData (output: OBDOutput) { 290 | const self = this; 291 | 292 | function doNextPoll (time: number) { 293 | self.log(`queueing next poll for ${time}ms from now`); 294 | 295 | if (self.pollTimer) { 296 | self.log('doNextPoll called before previous was cleared. possible timeout. clearing'); 297 | clearTimeout(self.pollTimer); 298 | } 299 | 300 | self.pollTimer = setTimeout(() => { 301 | if (self.curListener) { 302 | self.log(`poll timer was triggered, but we are already waiting on data. skipping`); 303 | } 304 | 305 | self.log(`poll timer triggered after ${time}ms`); 306 | 307 | self.addOutputListener(self.onPollLoopData.bind(self)); 308 | 309 | // Make sure we pay attention to possible timeouts in polling 310 | // A timeout is triggerd 1 second after the poll time if we get no data 311 | self.setTimeoutOperation(() => { 312 | self.log('timeout 1 sec after requesting data. retrying now'); 313 | doNextPoll(0); 314 | }, 1000); 315 | 316 | self.writeBytesToEcu() 317 | .catch((e: Error) => { 318 | self.log(`error performing next poll. retry in ${time}ms`); 319 | doNextPoll(time); 320 | }); 321 | }, time); 322 | } 323 | 324 | 325 | // If we have no timeoutTimer set then we should not be receiving any data! 326 | if (self.isMatchingPayload(output)) { 327 | self.log(`poller detected event that matched with self (${this.args.pid.getPid()}). payload is ${output.bytes}`); 328 | 329 | // Clear the timer 330 | self.pollTimer = undefined; 331 | 332 | // Stay on top of counts 333 | self.msgRecvCount++; 334 | 335 | // Let folks know we got data 336 | self.emit('data', output); 337 | 338 | // Track when we got this response 339 | self.lastResponseTs = Date.now(); 340 | 341 | // The emitted event is a match for this poller's PID 342 | self.log(`(${self.args.pid.getPid()}) received relevant data event (${output.bytes}) ${Date.now() - self.lastPollTs}ms after polling`); 343 | 344 | // No longer need to worry about timeouts 345 | self.unsetTimeoutOperation(); 346 | 347 | // Remove ouput listeners. We don't care data unless we requested it 348 | self.removeOutputListener(); 349 | 350 | // Queue next poll 351 | doNextPoll(self.getNextPollDelay()); 352 | } else { 353 | self.log(`detected event (${output.bytes}) but it was not a match`); 354 | } 355 | } 356 | 357 | /** 358 | * Stops the polling process and cancels any polls about to be queued 359 | * @return {void} 360 | */ 361 | public stopPolling () { 362 | this.log('cacelling poll interval for %s', this.args.pid.getName()); 363 | 364 | this.polling = false; 365 | 366 | this.unsetTimeoutOperation(); 367 | 368 | if (this.pollTimer) { 369 | clearTimeout(this.pollTimer); 370 | this.pollTimer = undefined; 371 | } 372 | }; 373 | } 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | -------------------------------------------------------------------------------- /lib/pids/pid.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as R from 'ramda'; 4 | import * as c from 'case'; 5 | import * as assert from 'assert'; 6 | import * as conversions from './conversions'; 7 | import { format } from 'util'; 8 | import { OBD_MESSAGE_TYPES } from '../constants'; 9 | import { PIDArgs } from '../interfaces'; 10 | 11 | 12 | /** 13 | * Parses a hexadecimal string to regular base 10 14 | * @param {String} byte 15 | * @return {Number} 16 | */ 17 | function parseHexToDecimal (byte: string) { 18 | return parseInt(byte, 16); 19 | } 20 | 21 | function leftpad (input: string, desiredLen: number): string { 22 | const padding = new Array(desiredLen - input.length + 1); 23 | 24 | return padding.join('0') + input; 25 | } 26 | 27 | /** 28 | * Used to create PID instances that will parse OBD data 29 | * @constructor 30 | * @param {Object} opts 31 | */ 32 | export abstract class PID { 33 | 34 | private constname: string; 35 | private fullpid: string; 36 | private opts: PIDArgs; 37 | 38 | protected maxRandomValue: number = 255; 39 | protected minRandomValue: number = 0; 40 | 41 | public constructor (opts: PIDArgs) { 42 | assert(opts.bytes > 0, 'opts.bytes for PID must be above 0'); 43 | 44 | // This can be used to identify this PID 45 | this.constname = c.constant(opts.name); 46 | 47 | // Save these for use 48 | this.opts = opts; 49 | } 50 | 51 | protected getRandomInt (min: number, max: number): number { 52 | return Math.floor(Math.random() * (max - min + 1)) + min; 53 | } 54 | 55 | public getRandomBytes (min?: number, max?: number): string[] { 56 | min = min || this.minRandomValue; 57 | max = max || this.maxRandomValue; 58 | 59 | // ensure random value is int, then convert to hex 60 | return [this.getRandomInt(min, max).toString(16)]; 61 | } 62 | 63 | public getName () { 64 | return this.opts.name; 65 | } 66 | 67 | public getPid () { 68 | return this.opts.pid; 69 | } 70 | 71 | /** 72 | * Returns the number of bytes that should should be extracted for 73 | * parsing in a payload of this PID type 74 | */ 75 | public getParseableByteCount () { 76 | return this.opts.bytes; 77 | } 78 | 79 | 80 | /** 81 | * Returns a prettier representation of a value that this PID represents, by 82 | * using the passed "units" value for the PID 83 | * 84 | * e.g f(10) => 10% 85 | * e.g f(55) => 55°C 86 | * 87 | * @param {Number} value 88 | * @return {String} 89 | */ 90 | public getFormattedValueForBytes (bytes: string[]) : string { 91 | let val = this.getValueForBytes(bytes); 92 | 93 | if (this.opts.unit) { 94 | return format('%s%s', val, this.opts.unit); 95 | } else { 96 | return val.toString(); 97 | } 98 | } 99 | 100 | 101 | /** 102 | * Generates the code that should be written to the ECU for querying this PID 103 | * Example is "010C" (CURRENT_DATA + "OC") for the engine RPM 104 | * 105 | * @return {String} 106 | */ 107 | public getWriteString () : string { 108 | return this.opts.mode + this.opts.pid; 109 | } 110 | 111 | 112 | /** 113 | * The default conversion function for each PID. It will convert a byte value 114 | * to a number. 115 | * 116 | * Many PIDs will override this since more involved conversions are required 117 | * 118 | * @return {Number} 119 | */ 120 | public getValueForBytes (bytes:string[]) : number|string { 121 | return conversions.parseHexToDecimal(bytes[1]); 122 | } 123 | 124 | 125 | /** 126 | * Given an input string of bytes, this will return them as pairs 127 | * e.g AE01CD => ['AE', '01', 'CD'] 128 | */ 129 | private getByteGroupings (str: string) : Array { 130 | let byteGroups:Array = []; 131 | 132 | for (let i = 0; i < str.length; i+=2) { 133 | byteGroups.push( 134 | str.slice(i, i+2) 135 | ); 136 | } 137 | 138 | return byteGroups; 139 | } 140 | } 141 | 142 | export class FuelLevel extends PID { 143 | constructor () { 144 | super({ 145 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 146 | pid: '2F', 147 | bytes: 1, 148 | name: 'Fuel Level Input', 149 | min: 0, 150 | max: 100, 151 | unit: '%' 152 | }) 153 | } 154 | 155 | public getValueForBytes (bytes: string[]): number { 156 | return conversions.percentage(bytes[2]); 157 | } 158 | } 159 | 160 | export class Rpm extends PID { 161 | constructor () { 162 | super({ 163 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 164 | pid: '0C', 165 | bytes: 2, 166 | name: 'Engine RPM', 167 | min: 0, 168 | max: 16383.75, 169 | unit: 'rpm' 170 | }) 171 | } 172 | 173 | public getValueForBytes (bytes: string[]): number { 174 | const a:number = parseHexToDecimal(bytes[2]) * 256; 175 | const b:number = parseHexToDecimal(bytes[3]); 176 | 177 | return (a + b) / 4; 178 | } 179 | 180 | public getRandomBytes (): string[] { 181 | // ensure random value is int, then convert to hex 182 | return [ 183 | this.getRandomInt(0, 255).toString(16), 184 | this.getRandomInt(0, 255).toString(16) 185 | ]; 186 | } 187 | } 188 | 189 | export class CoolantTemp extends PID { 190 | constructor () { 191 | super({ 192 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 193 | pid: '05', 194 | bytes: 1, 195 | name: 'Engine Coolant Temperature', 196 | min: -40, 197 | max: 215, 198 | unit: '°C' 199 | }); 200 | 201 | this.minRandomValue = 0; 202 | this.maxRandomValue = 255; // only a litte too fast... 203 | } 204 | 205 | public getValueForBytes (byte: string[]): number { 206 | return parseHexToDecimal(byte[2]) - 40; 207 | } 208 | } 209 | 210 | export class VehicleSpeed extends PID { 211 | constructor () { 212 | super({ 213 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 214 | pid: '0D', 215 | bytes: 1, 216 | name: 'Vehicle Speed', 217 | min: 0, 218 | max: 255, 219 | unit: 'km/h' 220 | }) 221 | 222 | this.minRandomValue = 0; 223 | this.maxRandomValue = 255; 224 | } 225 | 226 | public getValueForBytes (bytes: string[]): number { 227 | return parseHexToDecimal(bytes[2]); 228 | } 229 | } 230 | 231 | export class CalculatedEngineLoad extends PID { 232 | constructor () { 233 | super({ 234 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 235 | pid: '04', 236 | bytes: 1, 237 | name: 'Calculated Engine Load', 238 | min: 0, 239 | max: 100, 240 | unit: '%' 241 | }) 242 | 243 | this.minRandomValue = 0; 244 | this.maxRandomValue = 255; 245 | } 246 | 247 | public getValueForBytes (bytes: string[]): number { 248 | return parseHexToDecimal(bytes[2]) / 2.5; 249 | } 250 | } 251 | 252 | export class FuelPressure extends PID { 253 | constructor () { 254 | super({ 255 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 256 | pid: '0A', 257 | bytes: 1, 258 | name: 'Fuel Pressure', 259 | min: 0, 260 | max: 765, 261 | unit: 'kPa' 262 | }) 263 | 264 | this.minRandomValue = 0; 265 | this.maxRandomValue = 255; 266 | } 267 | 268 | public getValueForBytes (bytes: string[]): number { 269 | return parseHexToDecimal(bytes[2]) * 3; 270 | } 271 | } 272 | 273 | export class IntakeManifoldAbsolutePressure extends PID { 274 | constructor () { 275 | super({ 276 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 277 | pid: '0B', 278 | bytes: 1, 279 | name: 'Intake Manifold Absolute Pressure', 280 | min: 0, 281 | max: 255, 282 | unit: 'kPa' 283 | }) 284 | 285 | this.minRandomValue = 0; 286 | this.maxRandomValue = 255; 287 | } 288 | 289 | public getValueForBytes (bytes: string[]): number { 290 | return parseHexToDecimal(bytes[2]); 291 | } 292 | } 293 | 294 | export class IntakeAirTemperature extends PID { 295 | constructor () { 296 | super({ 297 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 298 | pid: '0F', 299 | bytes: 1, 300 | name: 'Intake Air Temperature', 301 | min: -40, 302 | max: 215, 303 | unit: '°C' 304 | }) 305 | 306 | this.minRandomValue = 0; 307 | this.maxRandomValue = 255; 308 | } 309 | 310 | public getValueForBytes (bytes: string[]): number { 311 | return parseHexToDecimal(bytes[2]) - 40; 312 | } 313 | } 314 | 315 | export class MafAirFlowRate extends PID { 316 | constructor () { 317 | super({ 318 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 319 | pid: '10', 320 | bytes: 2, 321 | name: 'MAF Air Flow Rate', 322 | min: 0, 323 | max: 655.35, 324 | unit: 'grams/sec' 325 | }) 326 | 327 | this.minRandomValue = 0; 328 | this.maxRandomValue = 255; 329 | } 330 | 331 | public getRandomBytes (min?: number, max?: number): string[] { 332 | min = min || this.minRandomValue; 333 | max = max || this.maxRandomValue; 334 | 335 | // ensure random value is int, then convert to hex 336 | return [ 337 | this.getRandomInt(min, max).toString(16), 338 | this.getRandomInt(min, max).toString(16) 339 | ]; 340 | } 341 | 342 | public getValueForBytes (bytes: string[]): number { 343 | const a = parseHexToDecimal(bytes[2]); 344 | const b = parseHexToDecimal(bytes[3]); 345 | 346 | return ((256 * a) + b) / 100; 347 | } 348 | } 349 | 350 | export class ThrottlePosition extends PID { 351 | constructor () { 352 | super({ 353 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 354 | pid: '11', 355 | bytes: 1, 356 | name: 'Throttle Position', 357 | min: 0, 358 | max: 100, 359 | unit: '%' 360 | }) 361 | 362 | this.minRandomValue = 0; 363 | this.maxRandomValue = 255; 364 | } 365 | 366 | public getValueForBytes (bytes: string[]): number { 367 | return (100 / 255) * parseHexToDecimal(bytes[2]); 368 | } 369 | } 370 | 371 | export class ObdStandard extends PID { 372 | constructor () { 373 | super({ 374 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 375 | pid: '1C', 376 | bytes: 1, 377 | name: 'OBD Standard', 378 | min: 0, 379 | max: 255, 380 | unit: '' 381 | }) 382 | 383 | this.minRandomValue = 0; 384 | this.maxRandomValue = 34; 385 | } 386 | 387 | public getValueForBytes (bytes: string[]): string { 388 | const type = parseHexToDecimal(bytes[2]); 389 | const obdStandards:Object = require('./data/obd-spec-list.json'); 390 | 391 | return obdStandards[type] || 'Unknown'; 392 | } 393 | } 394 | 395 | export class FuelSystemStatus extends PID { 396 | private types:Object = { 397 | '1': 'Open loop due to insufficient engine temperature', 398 | '2': 'Closed loop, using oxygen sensor feedback to determine fuel mix', 399 | '4': 'Open loop due to engine load OR fuel cut due to deceleration', 400 | '8': 'Open loop due to system failure', 401 | '16': 'Closed loop, using at least one oxygen sensor but there is a ' + 402 | 'fault in the feedback system' 403 | }; 404 | 405 | constructor () { 406 | super({ 407 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 408 | pid: '03', 409 | bytes: 2, 410 | name: 'Fuel System Status', 411 | min: 0, 412 | max: 16, 413 | unit: '' 414 | }) 415 | 416 | this.minRandomValue = 0; 417 | this.maxRandomValue = 10; 418 | } 419 | 420 | public getRandomBytes (min?: number, max?: number): string[] { 421 | min = min || this.minRandomValue; 422 | max = max || this.maxRandomValue; 423 | 424 | // ensure random value is int, then convert to hex 425 | return [ 426 | this.getRandomInt(min, max).toString(16), 427 | this.getRandomInt(min, max).toString(16) 428 | ]; 429 | } 430 | 431 | public getValueForBytes (bytes: string[]): string { 432 | const typeA = parseHexToDecimal(bytes[2]); 433 | const typeB = parseHexToDecimal(bytes[3]); 434 | 435 | if (typeB) { 436 | const a = this.types[typeA] || 'Unknown'; 437 | const b = this.types[typeB] || 'Unknown'; 438 | return `System A: ${a}. System B: ${b}`; 439 | } else { 440 | return this.types[typeA.toString()]; 441 | } 442 | } 443 | } 444 | 445 | export class SupportedPids extends PID { 446 | constructor () { 447 | super({ 448 | mode: OBD_MESSAGE_TYPES.CURRENT_DATA, 449 | pid: '20', 450 | bytes: 4, 451 | name: 'Supported PIDs', 452 | unit: '' 453 | }) 454 | 455 | this.minRandomValue = 0; 456 | this.maxRandomValue = 255; 457 | } 458 | 459 | public getRandomBytes (min?: number, max?: number): string[] { 460 | min = min || this.minRandomValue; 461 | max = max || this.maxRandomValue; 462 | 463 | return [ 464 | this.getRandomInt(min, max).toString(16), 465 | this.getRandomInt(min, max).toString(16), 466 | this.getRandomInt(min, max).toString(16), 467 | this.getRandomInt(min, max).toString(16) 468 | ]; 469 | } 470 | 471 | public getValueForBytes (bytes: string[]): string { 472 | // Get all bytes after the initial message identifier byte 473 | const allBytes = bytes.join('').substr(2); 474 | const supportedPids:string[] = []; 475 | 476 | for (let i = 0; i