@stoprocent/bluetooth-hci-socket
Version:
Bluetooth HCI socket binding for Node.js
259 lines (213 loc) • 7.06 kB
JavaScript
/* jshint esversion: 8 */
const EventEmitter = require('events');
const debug = require('debug')('hci-uart');
const { SerialPort } = require('serialport');
const HciSerialParser = require('./uart/hci-serial-parser');
const async = require('async');
const HCI_COMMAND_PKT = 0x01;
const OGF_HOST_CTL = 0x03;
const OCF_RESET = 0x0003;
const OCF_SET_EVENT_FILTER = 0x0005;
const SET_EVENT_FILTER_CMD = OCF_SET_EVENT_FILTER | (OGF_HOST_CTL << 10);
const RESET_CMD = OCF_RESET | (OGF_HOST_CTL << 10);
class BluetoothHciSocket extends EventEmitter {
constructor (useFilter = false) {
super();
this._isUp = false;
this._useFilter = useFilter;
this._mode = null;
this._serialDevice = null;
this._queue = null;
this._parser = null;
this._exitHandler = this.reset.bind(this);
}
setFilter (filter) {
if (!this._useFilter) {
return;
}
const header = Buffer.alloc(4);
header.writeUInt8(HCI_COMMAND_PKT, 0);
header.writeUInt16LE(SET_EVENT_FILTER_CMD, 1);
header.writeUInt8(filter.length, 3);
const cmd = Buffer.concat([header, filter]);
this.write(cmd);
}
bindRaw (devId, params) {
this.bindUser(devId, params, 'raw');
this.reset();
}
bindUser (devId, params = {}, mode = 'user') {
this._mode = mode;
const uartParams = this._getSerialParams(params);
const { port, baudRate, retryConnection = 10, flowControl = true } = uartParams.uart;
if (typeof port !== 'string' || !Number.isInteger(baudRate)) {
throw new Error('Invalid UART parameters');
}
debug(`Using UART PORT = ${port}, BAUD RATE = ${baudRate}`);
this._serialDevice = new SerialPort({
path: port,
baudRate,
autoOpen: false,
flowControl
});
if (!this._serialDevice) {
throw new Error('No compatible UART device found!');
}
this._queue = async.queue((data, callback) => {
this._serialDevice.write(data, callback);
});
this._queue.pause();
this._hciSerialParser = new HciSerialParser();
this._serialDevice.pipe(this._hciSerialParser);
this._hciSerialParser.on('raw', this.waitForReset.bind(this));
this._serialDevice.on('error', (error) => this.emit('error', error));
this._serialDevice.on('close', () => {
this._isUp = false;
this._isReconnectionCancelled = false;
this._serialDevice.unpipe(this._hciSerialParser);
this.emit('state', this._isUp);
this._handleReconnection(devId, params, mode, port, retryConnection);
});
}
getDeviceList () {
return SerialPort.list().then(ports => ports.map(port => ({
devId: null,
devUp: null,
idVendor: port.vendorId,
idProduct: port.productId,
path: port.path
})));
}
async _handleReconnection (devId, params, mode, port, maxRetries) {
let retryCount = 0;
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
this._isReconnectionCancelled = false; // Reset cancellation flag on new attempt
while (retryCount < maxRetries && !this._isReconnectionCancelled) {
try {
const ports = await SerialPort.list();
/* jshint loopfunc: true */
const portExists = ports.some(p => p.path === port);
if (portExists) {
debug(`Reconnecting to ${port}`);
this.bindUser(devId, params, mode);
this.start();
this.reset();
return;
}
retryCount++;
debug(`Retry ${retryCount}/${maxRetries} - Port ${port} not found`);
await delay(1000);
} catch (err) {
debug('Error listing ports:', err);
retryCount++;
if (retryCount >= maxRetries || this._isReconnectionCancelled) {
debug(`Reconnection stopped: ${this._isReconnectionCancelled ? 'Cancelled' : `Max retries (${maxRetries}) reached`} for port ${port}`);
return;
}
await delay(1000);
}
}
if (!this._isReconnectionCancelled) {
debug(`Max retries (${maxRetries}) reached for port ${port}`);
}
}
_getSerialParams (params) {
let port;
let baudRate = 1000000; // Default baud rate
// Check for UART port in environment variables
if (process.env.BLUETOOTH_HCI_SOCKET_UART_PORT) {
port = process.env.BLUETOOTH_HCI_SOCKET_UART_PORT;
}
// Check for UART baud rate in environment variables
if (process.env.BLUETOOTH_HCI_SOCKET_UART_BAUDRATE) {
const parsedBaudRate = parseInt(process.env.BLUETOOTH_HCI_SOCKET_UART_BAUDRATE, 10);
if (!isNaN(parsedBaudRate)) {
baudRate = parsedBaudRate;
}
}
// Override with params if provided
if (params && params.uart) {
if (params.uart.port && typeof params.uart.port === 'string') {
port = params.uart.port;
}
if (params.uart.baudRate && typeof params.uart.baudRate === 'number' && isFinite(params.uart.baudRate)) {
baudRate = params.uart.baudRate;
}
}
return { uart: { port, baudRate } };
}
bindControl () {
this._mode = 'control';
}
isDevUp () {
return this._isUp;
}
waitForReset (data) {
const resetPatterns = [
Buffer.from('040e0401030c00', 'hex'),
Buffer.from('040e0402030c00', 'hex'),
Buffer.from('040e0405030c00', 'hex')
];
if (
!this._isUp &&
this._mode === 'raw' &&
resetPatterns.some((pattern) => data.includes(pattern))
) {
debug('Reset complete');
this._hciSerialParser.removeAllListeners('raw');
this._hciSerialParser.reset();
this._queue.resume();
this._isUp = true;
this.emit('state', this._isUp);
}
}
start () {
if (this._mode !== 'raw' && this._mode !== 'user') {
return;
}
if (!this._serialDevice) {
throw new Error('Serial device is not initialized');
}
if (!this._serialDevice.isOpen) {
this._serialDevice.open();
process.on('exit', this._exitHandler);
}
this._hciSerialParser.removeAllListeners('data');
this._hciSerialParser.on('data', (data) => {
if (this._isUp) {
this.emit('data', data);
}
});
}
stop () {
// Remove the exit listener
process.removeListener('exit', this._exitHandler);
if (this._mode !== 'raw' && this._mode !== 'user') {
return;
}
if (this._serialDevice.isOpen) {
this._serialDevice.close();
}
this._isReconnectionCancelled = true;
this._hciSerialParser.removeAllListeners('data');
this._serialDevice.removeAllListeners();
}
write (data) {
debug(`Write: ${data.toString('hex')}`);
if (this._mode === 'raw' || this._mode === 'user') {
this._queue.push(data);
}
}
reset () {
if (!this._serialDevice) {
return;
}
const cmd = Buffer.alloc(4);
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(RESET_CMD, 1);
cmd.writeUInt8(0x00, 3);
debug(`Reset: ${cmd.toString('hex')}`);
this._serialDevice.write(cmd);
}
}
module.exports = BluetoothHciSocket;