UNPKG

ble-mesh-serial-interface-js

Version:

An npm package for Node.js that provides an API to control a router node in a BLE mesh network via the serial port.

511 lines (416 loc) 15.4 kB
'use strict'; const EventEmitter = require('events'); const SerialPort = require('serialport'); const commandOpCodes = { 'ECHO': 0x02, 'RADIO_RESET': 0x0e, 'INIT': 0x70, 'START': 0x74, 'STOP': 0x75, 'VALUE_SET': 0x71, 'VALUE_ENABLE': 0x72, 'VALUE_DISABLE': 0x73, 'FLAG_SET': 0x76, 'FLAG_GET': 0x77, 'DFU_DATA': 0x78, 'VALUE_GET': 0x7a, 'BUILD_VERSION_GET': 0x7b, 'ACCESS_ADDR_GET': 0x7c, 'CHANNEL_GET': 0x7d, 'INTERVAL_MIN_GET': 0x7f }; const smartMeshCommandOpCodes = { 'GET_VERSION': 0x50, 'SET_KEYPAIR': 0x51, // TODO: these may change in release... 'SET_CAPABILITIES': 0x52, 'SET_UUID': 0x53, /* Provisioning */ 'SERIAL_CMD_RANGE_PROV_START': 0x60, 'SERIAL_CMD_PROV_INIT_CONTEXT': 0x60, 'SERIAL_CMD_PROV_SCAN_START': 0x61, 'SERIAL_CMD_PROV_SCAN_STOP': 0x62, 'SERIAL_CMD_PROV_PROVISION': 0x63, 'SERIAL_CMD_PROV_LISTEN': 0x64, 'SERIAL_CMD_PROV_ACCEPT': 0x65, 'SERIAL_CMD_PROV_OOB_USE': 0x66, 'SERIAL_CMD_PROV_AUTH_DATA': 0x67, 'SERIAL_CMD_PROV_ECDH_SECRET': 0x68, 'SERIAL_CMD_PROV_SET_KEYPAIR': 0x69, 'SERIAL_CMD_PROV_SET_CAPABILITIES': 0x6A, 'SERIAL_CMD_RANGE_PROV_END': 0x6F } const responseOpCodes = { // TODO: add events for smart mesh. 'DEVICE_STARTED': 0x81, 'ECHO_RSP': 0x82, 'CMD_RSP': 0x84, 'EVENT_NEW': 0xB3, 'EVENT_UPDATE': 0xB4, 'EVENT_CONFLICTING': 0xB5, 'EVENT_TX': 0xB6, 'EVENT_DFU': 0x78 }; const statusCodes = { // TODO: add status codes for smart mesh. 'SUCCESS': 0x0, 'ERROR_UNKNOWN': 0x80, 'ERROR_INTERNAL': 0x81, 'ERROR_CMD_UNKNOWN': 0x82, 'ERROR_DEVICE_STATE_INVALID': 0x83, 'ERROR_INVALID_LENGTH': 0x84, 'ERROR_INVALID_PARAMETER': 0x85, 'ERROR_BUSY': 0x86, 'ERROR_INVALID_DATA': 0x87, 'ERROR_PIPE_INVALID': 0x90, 'RESERVED_START': 0xF0, 'RESERVED_END': 0xFF }; const reverseLookup = obj => val => { for (let k of Object.keys(obj)) if (obj[k] === val) return k; } const commandOpCodeToString = reverseLookup(commandOpCodes); const statusCodeToString = reverseLookup(statusCodes); const responseOpCodeToString = reverseLookup(responseOpCodes); class BLEMeshSerialInterface extends EventEmitter { constructor() { super(); this._callback; this._port; this._responseQueue = []; this._tempBuildResponse; } _serialPortInterface(serialPort, callback, baudRate, rtscts) { this._port = new SerialPort(serialPort, {baudRate: baudRate, rtscts: rtscts}, callback); this._port.on('data', data => { this.buildResponse(data); while (this._responseQueue.length !== 0) { const response = this._responseQueue.shift(); if (this.isCommandResponse(response)) { this._handleCommandResponse(response); } else { this._handleEventResponse(response); } } }); this._port.on('disconnect', err => { this.emit('disconnected', err); console.log('serial port disconnected: ', err); }); this._port.on('error', err => { this.emit('error', err); console.log('serial port error: ', err); }); } bufferToArray(buf) { let array = []; for (let i = 0; i < buf.length; i++) { array[i] = buf[i]; } return array; } buildResponse(data) { let length = data[0] + 1; // If we are in the middle of building a response this will be incorrect, and re-assigned below. if (!this._tempBuildResponse) { this._tempBuildResponse = new Buffer([]); } else { length = this._tempBuildResponse[0] + 1; } let remainingLength = length - this._tempBuildResponse.length; if (remainingLength >= data.length) { this._tempBuildResponse = Buffer.concat([this._tempBuildResponse, data]); } else { this._tempBuildResponse = Buffer.concat([this._tempBuildResponse, data.slice(0, remainingLength)]); } if (length === this._tempBuildResponse.length) { const response = new Buffer(this._tempBuildResponse); this._responseQueue.push(response); this._tempBuildResponse = null; } if (remainingLength < data.length) { this.buildResponse(data.slice(remainingLength)); } } /** * _byte(0xDEADBEEF, 0) => 0xEF and _byte(0xDEADBEEF, 3) => 0xDE. */ _byte(val, index) { return ((val >> (8 * index)) & 0xFF); } bytesArrayToUnsignedInt(array) { array.reverse(); // If handle is 0x01, array is [0x0, 0x1] and really we want array[0] << 8... if (array.length > 4) { console.log('error, max length of array to convert to an unsigned int is 4 bytes'); return -1; } let temp = 0; for (let i = 0; i < array.length; i++) { temp += (array[i] << (i * 8)) >>> 0; } return temp; } _handleCommandResponse(resp) { let response = this.bufferToArray(resp); if (response[0] === 0 & response.length === 1) { if (this._callback) { this._callback(null, response); } return; } const responseOpCode = response[1]; const commandOpCode = response[2]; const statusCode = response[3]; switch(responseOpCode) { case responseOpCodes.ECHO_RSP: this._callback(null, this.bufferToArray(response.slice(2))); break; case responseOpCodes.CMD_RSP: switch(statusCode) { case statusCodes.SUCCESS: switch (commandOpCode) { case commandOpCodes.FLAG_GET: this._callback(null, {handle: this.bytesArrayToUnsignedInt(response.slice(4, 6).reverse()), flagIndex: response[6], flagValue: response[7]} ); break; case commandOpCodes.VALUE_GET: this._callback(null, {handle: this.bytesArrayToUnsignedInt(response.slice(4, 6).reverse()), data: response.slice(6).reverse()} ); break; case commandOpCodes.ACCESS_ADDR_GET: this._callback(null, {accessAddr: this.bytesArrayToUnsignedInt(response.slice(4).reverse())} ); break; case commandOpCodes.CHANNEL_GET: this._callback(null, {channel: response[4]} ); break; case commandOpCodes.INTERVAL_MIN_GET: this._callback(null, {intervalMin: this.bytesArrayToUnsignedInt(response.slice(4).reverse())} ); break; default: // TODO: do we want to return build version get as an object? this._callback(null, response.slice(4)); } break; default: this._callback(new Error(`received a status code in the command response indicating an error ${statusCodeToString(statusCode)}`), response); } break; default: this._callback(new Error(`unknown command response opCode ${responseOpCodeToString(responseOpCode)}`), response); } } _handleEventResponse(response) { const data = this.bufferToArray(response); const responseOpCode = data[1]; switch(responseOpCode) { case responseOpCodes.DEVICE_STARTED: this.emit('deviceStarted', {operatingMode: response[2], hwError: response[3], dataCreditAvailable: response[4]} ); break; case responseOpCodes.EVENT_NEW: this.emit('eventNew', {handle: this.bytesArrayToUnsignedInt(data.slice(2, 4).reverse()), data: data.slice(4).reverse()} ); break; case responseOpCodes.EVENT_UPDATE: this.emit('eventUpdate', {handle: this.bytesArrayToUnsignedInt(data.slice(2, 4).reverse()), data: data.slice(4).reverse()} ); break; case responseOpCodes.EVENT_CONFLICTING: this.emit('eventConflicting', {handle: this.bytesArrayToUnsignedInt(data.slice(2, 4).reverse()), data: data.slice(4).reverse()} ); break; case responseOpCodes.EVENT_TX: this.emit('eventTX', {handle: this.bytesArrayToUnsignedInt(data.slice(2, 4).reverse()), data: data.slice(4).reverse()} ); break; case responseOpCodes.EVENT_DFU: this.emit('eventDFU', data); break; default: console.log('unknown event response received from slave device: ', response, responseOpCode, responseOpCodeToString(responseOpCode) ); } } isCommandResponse(response) { const opCode = response[1]; if (opCode === responseOpCodes.ECHO_RSP | opCode === responseOpCodes.CMD_RSP | response[0] === 0x00) { return true; } return false; } /* API Methods */ closeSerialPort(callback) { this._port.close(err => { if (err) { console.log('error in BLEMeshSerialInterface.closeSerialPort(): ', err); } callback(err); }); } openSerialPort(serialPort, callback, baudRate, rtscts) { if (typeof baudRate === 'undefined') { baudRate = 115200; } if (typeof rtscts === 'undefined') { rtscts = true; } if (typeof this._port !== 'undefined') { if (this._port.isOpen()) { this.closeSerialPort(err => { if (err) { throw err; } this._serialPortInterface(serialPort, callback, baudRate, rtscts); }); return; } } this._serialPortInterface(serialPort, callback, baudRate, rtscts); } writeSerialPort(data) { this._port.write(data, err => { if (err) { this.emit('error', err); console.log('error when writing to serial port: ', err.message); } }); } /* nRF Open Mesh Serial Interface */ echo(data, callback) { const buf = [data.length + 1, commandOpCodes.ECHO]; const command = new Buffer(buf.concat(data)); this._callback = callback; this.writeSerialPort(command); } init(accessAddr, intMinMS, channel, callback) { const command = new Buffer([10, commandOpCodes.INIT, this._byte(accessAddr, 0), this._byte(accessAddr, 1), this._byte(accessAddr, 2), this._byte(accessAddr, 3), this._byte(intMinMS, 0), this._byte(intMinMS, 1), this._byte(intMinMS, 2), this._byte(intMinMS, 3), channel]); this._callback = callback; this.writeSerialPort(command); } start(callback) { const command = new Buffer([1, commandOpCodes.START]); this._callback = callback; this.writeSerialPort(command); } stop(callback) { const command = new Buffer([1, commandOpCodes.STOP]); this._callback = callback; this.writeSerialPort(command); } valueSet(handle, data, callback) { const buf =[3 + data.length, commandOpCodes.VALUE_SET, this._byte(handle, 0), this._byte(handle, 1)]; const command = new Buffer(buf.concat(Array.from(data).reverse())); this._callback = callback; this.writeSerialPort(command); } valueGet(handle, callback) { const command = new Buffer([3, commandOpCodes.VALUE_GET, this._byte(handle, 0), this._byte(handle, 1)]); this._callback = callback; this.writeSerialPort(command); } valueEnable(handle, callback) { const command = new Buffer([3, commandOpCodes.VALUE_ENABLE, this._byte(handle, 0), this._byte(handle, 1)]); this._callback = callback; this.writeSerialPort(command); } valueDisable(handle, callback) { const command = new Buffer([3, commandOpCodes.VALUE_DISABLE, this._byte(handle, 0), this._byte(handle, 1)]); this._callback = callback; this.writeSerialPort(command); } buildVersionGet(callback) { const command = new Buffer([1, commandOpCodes.BUILD_VERSION_GET]); this._callback = callback; this.writeSerialPort(command); } accessAddrGet(callback) { const command = new Buffer([1, commandOpCodes.ACCESS_ADDR_GET]); this._callback = callback; this.writeSerialPort(command); } channelGet(callback) { const command = new Buffer([1, commandOpCodes.CHANNEL_GET]); this._callback = callback; this.writeSerialPort(command); } intervalMinGet(callback) { const command = new Buffer([1, commandOpCodes.INTERVAL_MIN_GET]); this._callback = callback; this.writeSerialPort(command); } radioReset(callback) { const command = new Buffer([1, commandOpCodes.RADIO_RESET]); this._callback = callback; this.writeSerialPort(command); } flagSet(handle, value, callback) { const buf = [5, commandOpCodes.FLAG_SET]; const command = new Buffer(buf.concat([this._byte(handle, 0), this._byte(handle, 1), 0, value])); this._callback = callback; this.writeSerialPort(command); } flagGet(handle, callback) { const buf = [4, commandOpCodes.FLAG_GET]; const command = new Buffer(buf.concat([this._byte(handle, 0), this._byte(handle, 1), 0])); this._callback = callback; this.writeSerialPort(command); } txEventSet(handle, value, callback) { const buf = [5, commandOpCodes.FLAG_SET]; const command = new Buffer(buf.concat([this._byte(handle, 0), this._byte(handle, 1), 1, value])); this._callback = callback; this.writeSerialPort(command); } txEventGet(handle, callback) { const buf = [4, commandOpCodes.FLAG_GET]; const command = new Buffer(buf.concat([this._byte(handle, 0), this._byte(handle, 1), 1])); this._callback = callback; this.writeSerialPort(command); } dfuData(data, callback) { const buf = [data.length + 1, commandOpCodes.DFU_DATA]; const command = new Buffer(buf.concat(data)); // Note: This is the only command where user is responsible for formatting data as little endian. this._callback = callback; this.writeSerialPort(command); } /* Smart Mesh Serial Interface */ getVersion(callback) { const command = new Buffer([1, smartMeshCommandOpCodes.GET_VERSION]); this._callback = callback; this.writeSerialPort(command); } setKeyPair(callback) { const command = new Buffer([1, smartMeshCommandOpCodes.SET_KEYPAIR]); this._callback = callback; this.writeSerialPort(command); } setCapabilities(callback) { const command = new Buffer([1, smartMeshCommandOpCodes.SET_CAPABILITIES]); this._callback = callback; this.writeSerialPort(command); } provInitContext(contextID, callback) { const command = new Buffer([2, smartMeshCommandOpCodes.SERIAL_CMD_PROV_INIT_CONTEXT, contextID]); this._callback = callback; this.writeSerialPort(command); } provStartScan(callback) { const command = new Buffer([1, smartMeshCommandOpCodes.SERIAL_CMD_PROV_SCAN_START]); this._callback = callback; this.writeSerialPort(command); } provStopScan(callback) { const command = new Buffer([1, smartMeshCommandOpCodes.SERIAL_CMD_PROV_SCAN_STOP]); this._callback = callback; this.writeSerialPort(command); } } module.exports = BLEMeshSerialInterface;