From 30517bbcb81920c9fd14c97251d6d855b8a0e633 Mon Sep 17 00:00:00 2001 From: Jacob Rosenthal Date: Thu, 27 Aug 2015 14:02:33 +0200 Subject: [PATCH] initial websocket bindings --- lib/bleno.js | 63 ++++++++------ lib/hci-socket/bindings.js | 5 ++ lib/mac/bindings.js | 12 ++- lib/websocket/bindings.js | 157 +++++++++++++++++++++++++++++++++ package.json | 1 + ws-slave.js | 173 +++++++++++++++++++++++++++++++++++++ 6 files changed, 379 insertions(+), 32 deletions(-) create mode 100644 lib/websocket/bindings.js create mode 100644 ws-slave.js diff --git a/lib/bleno.js b/lib/bleno.js index dc3f7fe1..f61600e3 100644 --- a/lib/bleno.js +++ b/lib/bleno.js @@ -11,10 +11,11 @@ var Characteristic = require('./characteristic'); var Descriptor = require('./descriptor'); var bindings = null; - var platform = os.platform(); -if (platform === 'darwin') { +if (process.env.BLENO_WEBSOCKET || process.title === 'browser') { + bindings = require('./websocket/bindings'); +} else if (platform === 'darwin') { bindings = require('./mac/bindings'); } else if (platform === 'linux' || platform === 'win32') { bindings = require('./hci-socket/bindings'); @@ -23,6 +24,7 @@ if (platform === 'darwin') { } function Bleno() { + this.platform = null; this.state = 'unknown'; this.address = 'unknown'; this.rssi = 0; @@ -38,7 +40,6 @@ function Bleno() { this._bindings.on('accept', this.onAccept.bind(this)); this._bindings.on('mtuChange', this.onMtuChange.bind(this)); this._bindings.on('disconnect', this.onDisconnect.bind(this)); - this._bindings.on('rssiUpdate', this.onRssiUpdate.bind(this)); } @@ -51,9 +52,21 @@ Bleno.prototype.Descriptor = Descriptor; Bleno.prototype.onStateChange = function(state) { debug('stateChange ' + state); + var self = this; + this.state = state; - this.emit('stateChange', state); + function emit(OSPlatform) { + self.platform = OSPlatform; + self.emit('stateChange', state); + } + + if(this.platform){ + this.emit('stateChange', state); + }else{ + self._bindings.once('OSPlatform', emit); + self._bindings.platform(); + } }; Bleno.prototype.onAddressChange = function(address) { @@ -129,23 +142,20 @@ Bleno.prototype.onAdvertisingStart = function(error) { this.emit('advertisingStart', error); }; -if (platform === 'linux') { - // Linux only API - Bleno.prototype.startAdvertisingWithEIRData = function(advertisementData, scanData, callback) { - if (callback) { - this.once('advertisingStart', callback); - } - this._bindings.startAdvertisingWithEIRData(advertisementData, scanData); - }; -} else if (platform === 'darwin' && parseFloat(os.release()) >= 14) { - // OS X >= 10.10 API - Bleno.prototype.startAdvertisingWithEIRData = function(advertisementData, callback) { - if (callback) { - this.once('advertisingStart', callback); - } - this._bindings.startAdvertisingWithEIRData(advertisementData); - }; -} +Bleno.prototype.startAdvertisingWithEIRData = function(advertisementData, scanData, callback) { + + var _callback = callback; + + if(typeof scanData === 'function') { + _callback = scanData; + } + + if (_callback) { + this.once('advertisingStart', _callback); + } + + this._bindings.startAdvertisingWithEIRData(advertisementData, scanData); +}; Bleno.prototype.stopAdvertising = function(callback) { if (callback) { @@ -176,13 +186,10 @@ Bleno.prototype.onServicesSet = function(error) { this.emit('servicesSet', error); }; -if (platform === 'linux') { - // Linux only API - Bleno.prototype.disconnect = function() { - debug('disconnect'); - this._bindings.disconnect(); - }; -} +Bleno.prototype.disconnect = function() { + debug('disconnect'); + this._bindings.disconnect(); +}; Bleno.prototype.updateRssi = function(callback) { if (callback) { diff --git a/lib/hci-socket/bindings.js b/lib/hci-socket/bindings.js index 6b068a52..90a4f153 100644 --- a/lib/hci-socket/bindings.js +++ b/lib/hci-socket/bindings.js @@ -2,6 +2,7 @@ var debug = require('debug')('bindings'); var events = require('events'); var util = require('util'); +var os = require('os'); var AclStream = require('./acl-stream'); var Hci = require('./hci'); @@ -204,6 +205,10 @@ BlenoBindings.prototype.onExit = function() { this.disconnect(); }; +BlenoBindings.prototype.platform = function(){ + this.emit('OSPlatform', os.platform()); +} + var blenoBindings = new BlenoBindings(); blenoBindings.init(); diff --git a/lib/mac/bindings.js b/lib/mac/bindings.js index da83508a..b84aa1bf 100644 --- a/lib/mac/bindings.js +++ b/lib/mac/bindings.js @@ -36,6 +36,10 @@ BlenoBindings.prototype.sendXpcMessage = function(message) { this._xpcConnection.sendMessage(message); }; +BlenoBindings.prototype.disconnect = function() { + debug('disconnect is a linux only api'); +}; + var blenoBindings = new BlenoBindings(); blenoBindings.on('xpcEvent', function(event) { @@ -47,10 +51,6 @@ blenoBindings.on('xpcEvent', function(event) { this.emit('kCBMsgId' + kCBMsgId, kCBMsgArgs); }); -blenoBindings.on('xpcError', function(message) { - console.error('xpcError: ' + message); -}); - blenoBindings.sendCBMsg = function(id, args) { debug('sendCBMsg: ' + id + ', ' + JSON.stringify(args, undefined, 2)); this.sendXpcMessage({ @@ -88,6 +88,10 @@ blenoBindings.init = function() { }.bind(this)); }; +blenoBindings.platform = function(){ + this.emit('OSPlatform', os.platform()); +} + blenoBindings.on('kCBMsgId6', function(args) { var state = ['unknown', 'resetting', 'unsupported', 'unauthorized', 'poweredOff', 'poweredOn'][args.kCBMsgArgState]; debug('state change ' + state); diff --git a/lib/websocket/bindings.js b/lib/websocket/bindings.js new file mode 100644 index 00000000..0a9e8604 --- /dev/null +++ b/lib/websocket/bindings.js @@ -0,0 +1,157 @@ +var events = require('events'); +var util = require('util'); + +var debug = require('debug')('bindings'); +var WebSocket = require('ws'); + +var BlenoBindings = function() { + var port = 0xB1f; + var ip = process.env.IP || 'localhost'; + + this._ws = new WebSocket('ws://' + ip + ':' + port); + + this._deviceUUID = null; + + this.on('message', this._onMessage.bind(this)); + + if (!this._ws.on) { + this._ws.on = this._ws.addEventListener; + } + + this._ws.on('open', this._onOpen.bind(this)); + this._ws.on('close', this._onClose.bind(this)); + this._ws.on('error', this._onError.bind(this)); + + var _this = this; + this._ws.on('message', function(event) { + var data = (process.title === 'browser') ? event.data : event; + + _this.emit('message', JSON.parse(data)); + }); +}; + +util.inherits(BlenoBindings, events.EventEmitter); + +BlenoBindings.prototype._onOpen = function() { +}; + +BlenoBindings.prototype._onClose = function() { + this.emit('stateChange', 'poweredOff'); +}; + +BlenoBindings.prototype._onError = function(error) { + this.emit('xpcError', error); +}; + +BlenoBindings.prototype._onMessage = function(event) { + var type = event.type; + var state = event.state; + var error = event.error; + var clientAddress = event.clientAddress; + var rssi = event.rssi; + var address = event.address; + var OSPlatform = event.OSPlatform; + + debug('on -> message: ' + JSON.stringify(event, undefined, 2)); + + if (type === 'stateChange') { + debug('state change ' + state); + this.emit('stateChange', state); + } else if (type === 'advertisingStart') { + this.emit('advertisingStart', error); + } else if (type === 'advertisingStartError') { + this.emit('advertisingStartError', error); + } else if (type === 'advertisingStop') { + this.emit('advertisingStop'); + } else if (type === 'servicesSet') { + this.emit('servicesSet', error); + } else if (type === 'servicesSetError') { + this.emit('servicesSetError', error); + } else if (type === 'accept') { + this._deviceUUID = clientAddress; + this.emit('accept', clientAddress); + } else if (type === 'disconnect') { + this.emit('disconnect', clientAddress); + } else if (type === 'rssiUpdate') { + this.emit('rssiUpdate', rssi); + } else if (type === 'addressChange') { + this.emit('addressChange', address); + } else if (type === 'OSPlatform'){ + this.emit('OSPlatform', OSPlatform); + } +}; + +BlenoBindings.prototype._sendCommand = function(command) { + debug('on -> sendMessage: ' + JSON.stringify(command, undefined, 2)); + var message = JSON.stringify(command); + this._ws.send(message); +}; + +BlenoBindings.prototype.disconnect = function() { + this._sendCommand({ + action: 'disconnect' + }); +}; + +var blenoBindings = new BlenoBindings(); + +blenoBindings.platform = function(){ + this._sendCommand({ + action: 'OSPlatform' + }); +} + +blenoBindings.startAdvertising = function(name, serviceUuids) { + this._sendCommand({ + action: 'startAdvertising', + name: name, + serviceUuids: serviceUuids + }); +}; + +blenoBindings.startAdvertisingIBeacon = function(data) { + this._sendCommand({ + action: 'startAdvertisingIBeacon', + data: data.toString('hex'), + }); +}; + +blenoBindings.startAdvertisingWithEIRData = function(advertisementData, scanData) { + if(!scanData){ + scanData = new Buffer([]); + } + + this._sendCommand({ + action: 'startAdvertisingWithEIRData', + advertisementData: advertisementData.toString('hex'), + scanData: scanData.toString('hex') + }); +}; + +blenoBindings.stopAdvertising = function() { + this._sendCommand({ + action: 'stopAdvertising' + }); +}; + +blenoBindings.setServices = function(services) { + // todo serialize services more fully + this._sendCommand({ + action: 'setServices', + services: services + }); +}; + + +blenoBindings.updateRssi = function() { + if (this._deviceUUID === null) { + this.emit('rssiUpdate', 127); // not supported + } else { + this._sendCommand({ + action: 'updateRssi', + deviceUUID: this._deviceUUID + }); + } +}; + +module.exports = blenoBindings; diff --git a/package.json b/package.json index 126e4aec..665ba90d 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "debug": "^2.2.0" }, "optionalDependencies": { + "ws": "^0.8.0", "bluetooth-hci-socket": "~0.3.1", "bplist-parser": "0.0.6", "xpc-connection": "~0.1.3" diff --git a/ws-slave.js b/ws-slave.js new file mode 100644 index 00000000..d9deb8a3 --- /dev/null +++ b/ws-slave.js @@ -0,0 +1,173 @@ +/* jshint loopfunc: true */ +var events = require('events'); + +var debug = require('debug')('blenoslave'); +var WebSocket = require('ws'); +var os = require('os'); + +var bleno = require('./index'); + +var serverMode = !process.argv[2]; +var port = 0xB1f; +var host = process.argv[2]; + + +var ws; +var wss; + +if (serverMode) { + debug('bleno - ws slave - server mode'); + wss = new WebSocket.Server({ + port: 0xB1f + }); + + wss.on('connection', function(ws_) { + debug('ws -> connection'); + + ws = ws_; + + sendEvent({ + type: 'stateChange', + state: bleno.state + }); + + ws.on('message', onMessage); + + ws.on('close', function() { + debug('ws -> close'); + + bleno.stopAdvertising(); + }); + }); +} else { + ws = new WebSocket('ws://' + host + ':' + port); + + ws.on('open', function() { + debug('ws -> open'); + }); + + ws.on('message', function(message) { + onMessage(message); + }); + + ws.on('close', function() { + debug('ws -> close'); + + bleno.stopAdvertising(); + }); +} + +var peripherals = {}; + +// TODO: open/close ws on state change + +function sendEvent(event) { + var message = JSON.stringify(event); + + debug('ws -> send: ' + message); + + var clients = serverMode ? wss.clients : [ws]; + + for (var i = 0; i < clients.length; i++) { + clients[i].send(message); + } +} + +var onMessage = function(message) { + debug('ws -> message: ' + message); + + var command = JSON.parse(message); + + var action = command.action; + var name = command.name; + var serviceUuids = command.serviceUuids; + var data = command.data ? new Buffer(command.data, 'hex') : null; + var advertisementData = command.advertisementData ? new Buffer(command.advertisementData, 'hex') : null; + var scanData = command.scanData ? new Buffer(command.scanData, 'hex') : null; + + //todo, deserialize services properly + var services = command.services; + + if (action === 'disconnect') { + bleno.disconnect(); + } else if (action === 'startAdvertising') { + bleno.startAdvertising(name, serviceUuids); + } else if (action === 'startAdvertisingIBeacon') { + bleno.startAdvertisingIBeacon(data); + } else if (action === 'startAdvertisingWithEIRData') { + bleno.startAdvertisingWithEIRData(advertisementData, scanData); + } else if (action === 'stopAdvertising') { + bleno.stopAdvertising(); + } else if (action === 'setServices') { + bleno.setServices(services); + } else if (action === 'updateRssi') { + bleno.updateRssi(); + } else if (action === 'OSPlatform') { + sendEvent({ + type: 'OSPlatform', + OSPlatform: bleno.platform + }); + } +}; + +bleno.on('addressChange', function(clientAddress){ + sendEvent({ + type: 'addressChange', + clientAddress: clientAddress + }); +}); + +bleno.on('advertisingStart', function(error){ + sendEvent({ + type: 'advertisingStart', + error: error + }); +}); + +bleno.on('advertisingStartError', function(error){ + sendEvent({ + type: 'advertisingStartError', + error: error + }); +}); + +bleno.on('advertisingStop', function(){ + sendEvent({ + type: 'advertisingStop' + }); +}); + +bleno.on('servicesSet', function(error){ + sendEvent({ + type: 'servicesSet', + error: error + }); +}); + +bleno.on('servicesSetError', function(error){ + sendEvent({ + type: 'servicesSetError', + error: error + }); +}); + +bleno.on('accept', function(clientAddress){ + sendEvent({ + type: 'accept', + clientAddress: clientAddress + }); +}); + +bleno.on('disconnect', function(clientAddress){ + sendEvent({ + type: 'disconnect', + clientAddress: clientAddress + }); +}); + +bleno.on('rssiUpdate', function(rssi){ + sendEvent({ + type: 'rssiUpdate', + rssi: rssi + }); +});