const BluetoothHciSocket = require('@abandonware/bluetooth-hci-socket'); var HCI_COMMAND_PKT = 0x01; var HCI_EVENT_PKT = 0x04; var EVT_LE_META_EVENT = 0x3e; var OGF_LE_CTL = 0x08; var OCF_LE_SET_SCAN_PARAMETERS = 0x000b; var OCF_LE_SET_SCAN_ENABLE = 0x000c; var OCF_LE_SET_ADVERTISING_PARAMETERS = 0x0006; var OCF_LE_SET_ADVERTISING_DATA = 0x0008; var OCF_LE_SET_ADVERTISE_ENABLE = 0x000a; var LE_SET_SCAN_PARAMETERS_CMD = OCF_LE_SET_SCAN_PARAMETERS | OGF_LE_CTL << 10; var LE_SET_SCAN_ENABLE_CMD = OCF_LE_SET_SCAN_ENABLE | OGF_LE_CTL << 10; var LE_SET_ADVERTISING_PARAMETERS_CMD = OCF_LE_SET_ADVERTISING_PARAMETERS | OGF_LE_CTL << 10; var LE_SET_ADVERTISING_DATA_CMD = OCF_LE_SET_ADVERTISING_DATA | OGF_LE_CTL << 10; var LE_SET_ADVERTISE_ENABLE_CMD = OCF_LE_SET_ADVERTISE_ENABLE | OGF_LE_CTL << 10; class PybricksBroadcast extends BluetoothHciSocket { constructor(...args) { super(...args); this.on('data', function(data) { //console.log(data.toString('hex')); if(data.length < 19) return; if(data.readUInt8(15) === 0xff) { // manufacturer data if(data.readUInt16LE(16) === 0x0397) { // lego this.emit('observe', {channel: data.readUInt8(18), data: data.slice(19, data.length -1), rssi: data.readInt8(data.length -1)}) } } }); this.on('error', function(error) {console.error(error);}); this.bindRaw(); var filter = Buffer.allocUnsafe(14).fill(0); var typeMask = (1 << HCI_EVENT_PKT); var eventMask2 = (1 << (EVT_LE_META_EVENT - 32)); filter.writeUInt32LE(typeMask, 0); filter.writeUInt32LE(eventMask2, 8); this.setFilter(filter); this.start(); this.setScanEnable(false); this.setAdvertiseEnable(false); this.setScanParameters(); this.setAdvertiseParameters(); this.setScanEnable(true); this.setAdvertiseEnable(true); } destructor() { this.setScanEnable(false); this.setAdvertiseEnable(false); this.stop(); } setScanParameters() { var cmd = Buffer.allocUnsafe(11); cmd.writeUInt8(HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(LE_SET_SCAN_PARAMETERS_CMD, 1); // command cmd.writeUInt8(0x07, 3); // length cmd.writeUInt8(0x00, 4); // type: 0 -> passive, 1 -> active cmd.writeUInt16LE(0x003e, 5); // internal, ms * 1.6 cmd.writeUInt16LE(0x003e, 7); // window, ms * 1.6 cmd.writeUInt8(0x00, 9); // own address type: 0 -> public, 1 -> random cmd.writeUInt8(0x00, 10); // filter: 0 -> all event types this.write(cmd); } setScanEnable(enabled) { var cmd = Buffer.allocUnsafe(6); cmd.writeUInt8(HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(LE_SET_SCAN_ENABLE_CMD, 1); // command cmd.writeUInt8(0x02, 3); // length cmd.writeUInt8(enabled ? 0x01 : 0x00, 4); // enable: 0 -> disabled, 1 -> enabled cmd.writeUInt8(0x01, 5); // duplicates: 0 -> no duplicates, 1 -> duplicates this.write(cmd); } setAdvertiseParameters() { var cmd = Buffer.allocUnsafe(19).fill(0); cmd.writeUInt8(HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(LE_SET_ADVERTISING_PARAMETERS_CMD, 1); // command cmd.writeUInt8(15, 3); // length cmd.writeUInt16LE(0x00a0, 4); // min interval cmd.writeUInt16LE(0x00a0, 6); // max interval cmd.writeUInt8(0x03, 8); // adv type: 3 -> ADV_NONCONN_IND cmd.writeUInt8(0x07, 17); this.write(cmd); }; setAdvertiseData(data) { var cmd = Buffer.allocUnsafe(36).fill(0); cmd.writeUInt8(HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(LE_SET_ADVERTISING_DATA_CMD, 1); // command cmd.writeUInt8(32, 3); // length cmd.writeUInt8(data.length, 4); data.copy(cmd, 5); this.write(cmd); } setAdvertiseEnable(enabled) { var cmd = Buffer.allocUnsafe(5).fill(0); cmd.writeUInt8(HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(LE_SET_ADVERTISE_ENABLE_CMD, 1); // command cmd.writeUInt8(0x01, 3); // length cmd.writeUInt8(enabled ? 0x01 : 0x00, 4); // enable: 0 -> disabled, 1 -> enabled this.write(cmd); } setBroadcast(channel, data) { var header = Buffer.allocUnsafe(5); header.writeUInt8(data.length + 4, 0); // length header.writeUInt8(0xff, 1); // manufacturer data header.writeUInt16LE(0x0397, 2); // lego header.writeUInt8(channel, 4); // pybricks channel nr this.setAdvertiseData(Buffer.concat([header, data])); } } broadcaster = new PybricksBroadcast(); broadcaster.on('observe', (arg) => {console.log(arg)}); broadcaster.setBroadcast(0, Buffer.from([0x61, 0x64, 0x84, 0x00, 0x00, 0x80, 0x3f, 0xA2, 0x68, 0x69, 0x20])); ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => process.on(signal, () => {broadcaster.destructor();}));