@jstarpl/bluetooth-obd
Version:
Package for communicating with a bluetooth OBD-II reader
409 lines (369 loc) • 13.9 kB
JavaScript
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* (C) Copyright 2013, TNO
* Author: Eric Smekens
*/
;
//Used for event emitting.
var EventEmitter = require('events').EventEmitter;
var util = require('util');
/**
* obdInfo.js for all PIDS.
* @type {*}
*/
var PIDS = require('../lib/obdInfo.js');
/**
* Constant for defining delay between writes.
* @type {number}
*/
var writeDelay = 50;
/**
* Queue for writing
* @type {Array}
*/
var queue = [];
// Class OBDReader
var OBDReader;
/**
* Creates an instance of OBDReader.
* @constructor
* @param {string} address MAC-address of device that will be connected to.
* @param {number} channel Channel that the serial port service runs on.
* @this {OBDReader}
*/
OBDReader = function () {
EventEmitter.call(this);
this.connected = false;
this.receivedData = "";
this.protocol = '0' ;
return this;
};
util.inherits(OBDReader, EventEmitter);
/**
* Find a PID-value by name.
* @param name Name of the PID you want the hexadecimal (in ASCII text) value of.
* @return {string} PID in hexadecimal ASCII
*/
function getPIDByName(name) {
var i;
for (i = 0; i < PIDS.length; i++) {
if (PIDS[i].name === name) {
if (PIDS[i].pid !== undefined) {
return (PIDS[i].mode + PIDS[i].pid);
}
//There are modes which don't require a extra parameter ID.
return (PIDS[i].mode);
}
}
}
/**
* Parses a hexadecimal string to a reply object. Uses PIDS. (obdInfo.js)
* @param {string} hexString Hexadecimal value in string that is received over the serialport.
* @return {Object} reply - The reply.
* @return {string} reply.value - The value that is already converted. This can be a PID converted answer or "OK" or "NO DATA".
* @return {string} reply.name - The name. --! Only if the reply is a PID.
* @return {string} reply.mode - The mode of the PID. --! Only if the reply is a PID.
* @return {string} reply.pid - The PID. --! Only if the reply is a PID.
*/
function parseOBDCommand(hexString) {
var reply,
byteNumber,
valueArray; //New object
reply = {};
if (hexString === "NO DATA" || hexString === "OK" || hexString === "?" || hexString === "UNABLE TO CONNECT" || hexString === "SEARCHING...") {
//No data or OK is the response, return directly.
reply.value = hexString;
return reply;
}
hexString = hexString.replace(/ /g, ''); //Whitespace trimming //Probably not needed anymore?
valueArray = [];
for (byteNumber = 0; byteNumber < hexString.length; byteNumber += 2) {
valueArray.push(hexString.substr(byteNumber, 2));
}
if (valueArray[0] === "41") {
reply.mode = valueArray[0];
reply.pid = valueArray[1];
for (var i = 0; i < PIDS.length; i++) {
if (PIDS[i].pid == reply.pid) {
var numberOfBytes = PIDS[i].bytes;
reply.name = PIDS[i].name;
switch (numberOfBytes) {
case 1:
reply.value = PIDS[i].convertToUseful(valueArray[2]);
break;
case 2:
reply.value = PIDS[i].convertToUseful(valueArray[2], valueArray[3]);
break;
case 4:
reply.value = PIDS[i].convertToUseful(valueArray[2], valueArray[3], valueArray[4], valueArray[5]);
break;
case 8:
reply.value = PIDS[i].convertToUseful(valueArray[2], valueArray[3], valueArray[4], valueArray[5], valueArray[6], valueArray[7], valueArray[8], valueArray[9]);
break;
}
break; //Value is converted, break out the for loop.
}
}
} else if (valueArray[0] === "43") {
reply.mode = valueArray[0];
for (var i = 0; i < PIDS.length; i++) {
if (PIDS[i].mode == "03") {
reply.name = PIDS[i].name;
reply.value = PIDS[i].convertToUseful(valueArray[1], valueArray[2], valueArray[3], valueArray[4], valueArray[5], valueArray[6]);
}
}
}
return reply;
}
/**
* Set the protocol version number to use with your car. Defaults to 0
* which is to autoselect.
*
* Uses the ATSP command - see http://www.obdtester.com/elm-usb-commands
*
* @default 0
*
*/
OBDReader.prototype.setProtocol = function (protocol) {
if(protocol.toString().search(/^[0-9]$/) === -1) {
throw "setProtocol: Must provide a number between 0 and 9 - refer to ATSP section of http://www.obdtester.com/elm-usb-commands";
}
this.protocol = protocol;
}
/**
* Get the protocol version number set for this object. Defaults to 0
* which is to autoselect.
*
* Uses the ATSP command - see http://www.obdtester.com/elm-usb-commands
*
*/
OBDReader.prototype.getProtocol = function () {
return this.protocol;
}
/**
* Attempts discovery of and subsequent connection to Bluetooth device and channel
* @param {string} query Query string to be fuzzy-ish matched against device name/address
*/
OBDReader.prototype.autoconnect = function (query) {
var self = this; //Enclosure
var btSerial = new (require('bluetooth-serial-port')).BluetoothSerialPort();
var search = new RegExp(query.replace(/\W/g, ''), 'gi');
btSerial.on('found', function (address, name) {
var addrMatch = !query || address.replace(/\W/g, '').search(search) != -1;
var nameMatch = !query || name.replace(/\W/g, '').search(search) != -1;
if (addrMatch || nameMatch) {
btSerial.removeAllListeners('finished');
btSerial.removeAllListeners('found');
self.emit('debug', 'Found device: ' + name + ' (' + address + ')');
btSerial.findSerialPortChannel(address, function (channel) {
self.emit('debug', 'Found device channel: ' + channel);
self.connect(address, channel);
}, function (err) {
console.log("Error finding serialport: " + err);
});
} else {
self.emit('debug', 'Ignoring device: ' + name + ' (' + address + ')');
}
});
btSerial.on('finished', function () {
self.emit('error', 'No suitable devices found');
});
btSerial.inquire();
}
/**
* Connect/Open the bluetooth serial port and add events to bluetooth-serial-port.
* Also starts the intervalWriter that is used to write the queue.
* @this {OBDReader}
*/
OBDReader.prototype.connect = function (address, channel) {
var self = this; //Enclosure
var btSerial = new (require('bluetooth-serial-port')).BluetoothSerialPort();
btSerial.connect(address, channel, function () {
self.connected = true;
self.write('ATZ');
//Turns off extra line feed and carriage return
self.write('ATL0');
//This disables spaces in in output, which is faster!
self.write('ATS0');
//Turns off headers and checksum to be sent.
self.write('ATH0');
//Turns off echo.
self.write('ATE0');
//Turn adaptive timing to 2. This is an aggressive learn curve for adjusting the timeout. Will make huge difference on slow systems.
self.write('ATAT2');
//Set timeout to 10 * 4 = 40msec, allows +20 queries per second. This is the maximum wait-time. ATAT will decide if it should wait shorter or not.
//self.write('ATST0A');
//http://www.obdtester.com/elm-usb-commands
self.write('ATSP'+self.protocol);
//Event connected
self.emit('connected');
btSerial.on('data', function (data) {
var currentString, arrayOfCommands;
currentString = self.receivedData + data.toString('utf8'); // making sure it's a utf8 string
arrayOfCommands = currentString.split('>');
var forString;
if (arrayOfCommands.length < 2) {
self.receivedData = arrayOfCommands[0];
} else {
for (var commandNumber = 0; commandNumber < arrayOfCommands.length; commandNumber++) {
forString = arrayOfCommands[commandNumber];
if (forString === '') {
continue;
}
var multipleMessages = forString.split('\r');
for (var messageNumber = 0; messageNumber < multipleMessages.length; messageNumber++) {
var messageString = multipleMessages[messageNumber];
if (messageString === '') {
continue;
}
var reply;
reply = parseOBDCommand(messageString);
//Event dataReceived.
self.emit('dataReceived', reply);
self.receivedData = '';
}
}
}
});
btSerial.on('failure', function (error) {
self.emit('error', 'Error with OBD-II device: ' + error);
});
}, function (err) { //Error callback!
self.emit('error', 'Error with OBD-II device: ' + err);
});
this.btSerial = btSerial; //Save the connection in OBDReader object.
this.intervalWriter = setInterval(function () {
if (queue.length > 0 && self.connected)
try {
self.btSerial.write(new Buffer(queue.shift(), "utf-8"), function (err, count) {
if (err)
self.emit('error', err);
});
} catch (err) {
self.emit('error', 'Error while writing: ' + err);
self.emit('error', 'OBD-II Listeners deactivated, connection is probably lost.');
clearInterval(self.intervalWriter);
self.removeAllPollers();
}
}, writeDelay); //Updated with Adaptive Timing on ELM327. 20 queries a second seems good enough.
return this;
};
/**
* Disconnects/closes the port.
*
* @param {Function} cb Callback function when the serial connection is closed
* @this {OBDReader}
*/
OBDReader.prototype.disconnect = function (cb) {
clearInterval(this.intervalWriter);
queue.length = 0; //Clears queue
if(typeof cb === 'function') {
this.btSerial.on('closed',cb) ;
}
this.btSerial.close();
this.connected = false;
};
/**
* Writes a message to the port. (Queued!) All write functions call this function.
* @this {OBDReader}
* @param {string} message The PID or AT Command you want to send. Without \r or \n!
* @param {number} replies The number of replies that are expected. Default = 0. 0 --> infinite
* AT Messages --> Zero replies!!
*/
OBDReader.prototype.write = function (message, replies) {
if (replies === undefined) {
replies = 0;
}
if (this.connected) {
if (queue.length < 256) {
if (replies !== 0) {
queue.push(message + replies + '\r');
} else {
queue.push(message + '\r');
}
} else {
this.emit('error', 'Queue-overflow!');
}
} else {
this.emit('error', 'Bluetooth device is not connected.');
}
};
/**
* Writes a PID value by entering a pid supported name.
* @this {OBDReader}
* @param {string} name Look into obdInfo.js for all PIDS.
*/
OBDReader.prototype.requestValueByName = function (name) {
this.write(getPIDByName(name));
};
var activePollers = [];
/**
* Adds a poller to the poller-array.
* @this {OBDReader}
* @param {string} name Name of the poller you want to add.
*/
OBDReader.prototype.addPoller = function (name) {
var stringToSend = getPIDByName(name);
activePollers.push(stringToSend);
};
/**
* Removes an poller.
* @this {OBDReader}
* @param {string} name Name of the poller you want to remove.
*/
OBDReader.prototype.removePoller = function (name) {
var stringToDelete = getPIDByName(name);
var index = activePollers.indexOf(stringToDelete);
activePollers.splice(index, 1);
};
/**
* Removes all pollers.
* @this {OBDReader}
*/
OBDReader.prototype.removeAllPollers = function () {
activePollers.length = 0; //This does not delete the array, it just clears every element.
};
/**
* Writes all active pollers.
* @this {OBDReader}
*/
OBDReader.prototype.writePollers = function () {
var i;
for (i = 0; i < activePollers.length; i++) {
this.write(activePollers[i], 1);
}
};
var pollerInterval;
/**
* Starts polling. Lower interval than activePollers * 50 will probably give buffer overflows. See writeDelay.
* @this {OBDReader}
* @param {number} interval Frequency how often all variables should be polled. (in ms). If no value is given, then for each activePoller 75ms will be added.
*/
OBDReader.prototype.startPolling = function (interval) {
if (interval === undefined) {
interval = activePollers.length * (writeDelay * 2); //Double the delay, so there's room for manual requests.
}
var self = this;
pollerInterval = setInterval(function () {
self.writePollers();
}, interval);
};
/**
* Stops polling.
* @this {OBDReader}
*/
OBDReader.prototype.stopPolling = function () {
clearInterval(pollerInterval);
};
var exports = module.exports = OBDReader;