├── .gitignore
├── Bluez.js
├── BluezAdapter.js
├── BluezAgent.js
├── BluezCharacteristic.js
├── BluezDBus.js
├── BluezDevice.js
├── BluezService.js
├── LICENSE
├── README.md
├── app.js
├── config.json
├── package.json
├── utils.js
└── utils
└── getGattAssignedNumbers.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.swo
3 | tags
4 |
5 | node_modules
6 |
7 | resources/characteristics.json
8 | resources/services.json
9 |
--------------------------------------------------------------------------------
/Bluez.js:
--------------------------------------------------------------------------------
1 | const events = require('events');
2 | const debug = require('debug')('Bluez');
3 | const bluezDBus = require('./BluezDBus');
4 | const BluezAdapter = require('./BluezAdapter');
5 |
6 | var bluez = new events.EventEmitter();
7 |
8 | function interfaceAdded(path, objects) {
9 | /* We're only interested in Adapters */
10 | if (objects['org.bluez.Adapter1'] === undefined)
11 | return;
12 |
13 | debug('An adapter was added: ' + path);
14 | var adapter = new BluezAdapter(path);
15 | adapter.init((err) => {
16 | if (err) {
17 | debug('Failed initializing new adapter ' + path + ': ' + err);
18 | return;
19 | }
20 | bluez.emit('adapter', adapter);
21 | });
22 | }
23 |
24 | bluezDBus.getAllObjects(function(err, objects) {
25 | if (err)
26 | throw 'Failed getting all objects';
27 |
28 | Object.keys(objects).forEach((key) => interfaceAdded(key, objects[key]));
29 | });
30 |
31 | bluezDBus.onInterfaces(interfaceAdded);
32 |
33 | module.exports = bluez;
34 |
--------------------------------------------------------------------------------
/BluezAdapter.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events').EventEmitter;
2 | const util = require('util');
3 | const _ = require('underscore');
4 | const bluezDBus = require('./BluezDBus');
5 | const BluezDevice = require('./BluezDevice');
6 |
7 | module.exports = BluezAdapter;
8 |
9 | function BluezAdapter(path){
10 | this.debug = require('debug')('Bluez:Adapter:' + path);
11 | this.path = path;
12 | this.debug('New adapter');
13 | }
14 |
15 | util.inherits(BluezAdapter, EventEmitter);
16 |
17 | BluezAdapter.prototype.init = function(cb) {
18 | /* Get Adapter1 interface */
19 | bluezDBus.getInterface(this.path, 'org.bluez.Adapter1', (err, iface) => {
20 | if (err) {
21 | this.debug('Failed getting Adapter1 interface:' + err);
22 | if (cb) cb(err);
23 | return;
24 | }
25 |
26 | /* Save this interface */
27 | this.iface = iface;
28 |
29 | /* Get Protperties interface */
30 | this.props = bluezDBus.getProperties(this.path, 'org.bluez.Adapter1',
31 | this._devicePropertiesUpdate.bind(this), /* Property changed */
32 | (err) => { /* All propertires were resolved */
33 | if (err) {
34 | this.debug('Failed getting all properties:' + err);
35 | if (cb) cb(err);
36 | return;
37 | }
38 |
39 | /* The new adapter is ready, wait 2 seconds until it settles down */
40 | if (cb) setTimeout(cb, 2000);
41 | });
42 | });
43 |
44 | /* Save the event handler so we can remove it later */
45 | this.ifaceEvents = bluezDBus.onInterfaces(this._interfaceAdded.bind(this),
46 | this._interfaceRemoved.bind(this));
47 | }
48 |
49 | BluezAdapter.prototype.toString = function() {
50 | return '[Object BluezAdapter (' + this.path + ')]';
51 | }
52 |
53 | BluezAdapter.prototype._isOwnDevice = function(path, objects) {
54 | /* If we're not powered, we don't care about cached devices yet */
55 | if (!this.Powered)
56 | return false;
57 |
58 | /* Make sure this device was discovered using this adapter */
59 | if (!path.startsWith(this.path))
60 | return false;
61 |
62 | /* We're only interested in devices */
63 | if (objects['org.bluez.Device1'] === undefined)
64 | return false;
65 |
66 | return true;
67 | }
68 |
69 | BluezAdapter.prototype._onPowerChanged = function(powered) {
70 | if (!powered)
71 | return;
72 |
73 | /* The adapter was powered on, now let's check if there are any relevant
74 | * objects (devices) */
75 | bluezDBus.getAllObjects((err, objects) => {
76 | if (err)
77 | throw 'Failed getting all objects';
78 |
79 | Object.keys(objects).forEach((key) => {
80 | this._interfaceAdded(key, objects[key]);
81 | });
82 | });
83 | }
84 |
85 | BluezAdapter.prototype._devicePropertiesUpdate = function(key, value) {
86 | if (_.isEqual(this[key], value))
87 | return;
88 |
89 | this.debug(key + ' changed from ' + this[key] + ' to ' + value);
90 | this[key] = value;
91 |
92 | if (key == 'Powered')
93 | this._onPowerChanged(value);
94 |
95 | this.emit('propertyChanged', key, value);
96 | }
97 |
98 | BluezAdapter.prototype._interfaceAdded = function(path, objects) {
99 | /* We're only interested in devices under this adapter */
100 | if (!this._isOwnDevice(path, objects))
101 | return;
102 |
103 | this.debug('A device was added: ' + path);
104 | var device = new BluezDevice(path);
105 | device.init((err) => {
106 | if (err) {
107 | this.debug('Failed initializing new device ' + path + ': ' + err);
108 | return;
109 | }
110 | this.emit('device', device);
111 | });
112 | }
113 |
114 | BluezAdapter.prototype._interfaceRemoved = function(path, objects) {
115 | /* We're only interested in ourselves */
116 | if (this.path !== path)
117 | return;
118 |
119 | this.emit('removed');
120 | this.removeAllListeners();
121 | this.props.close();
122 | this.ifaceEvents.close();
123 | }
124 |
125 | BluezAdapter.prototype.powerOn = function(cb) {
126 | this.iface.setProperty('Powered', true, (err) => {
127 | if (err)
128 | this.debug('Failed powering on:' + err);
129 | else
130 | this.debug('Powered on');
131 |
132 | if (cb) cb(err);
133 | });
134 | }
135 |
136 | BluezAdapter.prototype.powerOff = function(cb) {
137 | this.iface.setProperty('Powered', false, (err) => {
138 | if (err)
139 | this.debug('Failed powering off:' + err);
140 | else
141 | this.debug('Powered off');
142 |
143 | if (cb) cb(err);
144 | });
145 | }
146 |
147 | BluezAdapter.prototype.discoveryStart = function(cb) {
148 | this.iface.StartDiscovery['finish'] = () => {
149 | this.debug('Started discovering');
150 | if (cb) cb();
151 | };
152 | this.iface.StartDiscovery['error'] = (err) => {
153 | this.debug('Failed starting discovery: ' + err);
154 | if (cb) cb(err);
155 | };
156 | this.iface.StartDiscovery();
157 | }
158 |
159 | BluezAdapter.prototype.discoveryStop = function(cb) {
160 | this.iface.StopDiscovery['finish'] = () => {
161 | this.debug('Stopped discovering');
162 | if (cb) cb();
163 | };
164 | this.iface.StopDiscovery['error'] = (err) => {
165 | this.debug('Failed stopping discovery: ' + err);
166 | if (cb) cb(err);
167 | };
168 | this.iface.StopDiscovery();
169 | }
170 |
171 | BluezAdapter.prototype.discoveryFilterSet = function(filter, cb) {
172 | this.iface.SetDiscoveryFilter['finish'] = () => {
173 | this.debug('Set discovery filter:', filter);
174 | if (cb) cb();
175 | };
176 | this.iface.SetDiscoveryFilter['error'] = (err) => {
177 | this.debug('Failed setting discovery filter: ' + err);
178 | if (cb) cb(err);
179 | };
180 | this.iface.SetDiscoveryFilter(filter);
181 | }
182 |
183 | BluezAdapter.prototype.removeDevice = function(device, cb) {
184 | this.iface.RemoveDevice['finish'] = () => {
185 | this.debug('Removed device');
186 | if (cb) cb();
187 | };
188 | this.iface.RemoveDevice['error'] = (err) => {
189 | this.debug('Failed removing device: ' + err);
190 | if (cb) cb(err);
191 | };
192 | this.iface.RemoveDevice(device.path);
193 | }
194 |
--------------------------------------------------------------------------------
/BluezAgent.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events').EventEmitter;
2 | const util = require('util');
3 | const DBus = require('dbus');
4 | const dbus = new DBus();
5 | const bluezDBus = require('./BluezDBus');
6 |
7 | module.exports = BluezAgent;
8 |
9 | function BluezAgent(serviceName, path, onRegistered) {
10 | this.debug = require('debug')('Bluez:Agent:' + path);
11 | this.path = path;
12 | this.debug('New agent');
13 |
14 | /* Create service based on org.bluez.Agent1 definition. We only support BLE
15 | * devices at the moment and can only provide a passkey */
16 | var service = dbus.registerService('system', serviceName);
17 | var obj = service.createObject(path);
18 | var iface = obj.createInterface('org.bluez.Agent1');
19 |
20 | iface.addMethod('Release', {}, function(callback) {
21 | this.debug('Release()');
22 | callback();
23 | });
24 |
25 | iface.addMethod('RequestPasskey', { in: [{ type: 'o' }], out: { type: 'u' } },
26 | (device, callback) => {
27 | this.debug('RequestPasskey(' + device + ')');
28 | var passkey = null;
29 | if (this.passkeyHandler !== undefined)
30 | passkey = this.passkeyHandler(device);
31 |
32 | this.debug('Providing passkey ' + passkey + ' for ' + device);
33 | callback(passkey ? passkey : new Error('org.bluez.Error.Canceled'));
34 | }
35 | );
36 |
37 | iface.addMethod('Cancel', {}, function(callback) {
38 | debug('Cancel()');
39 | callback();
40 | });
41 |
42 | iface.update();
43 | }
44 |
45 | util.inherits(BluezAgent, EventEmitter);
46 |
47 | BluezAgent.prototype.register = function(cb) {
48 | /* Register agent with Bluez */
49 | bluezDBus.getInterface('/org/bluez', 'org.bluez.AgentManager1', (err, iface) =>
50 | {
51 | if (err) {
52 | this.debug('Failed getting AgentManager1 interface:' + err);
53 | return;
54 | }
55 |
56 | /* Save this interface */
57 | this.iface = iface;
58 |
59 | iface.RegisterAgent['finish'] = () => {
60 | this.debug('Registered agent');
61 | if (cb) cb();
62 | };
63 | iface.RegisterAgent['error'] = (err) => {
64 | this.debug('Failed registered agent: ' + err);
65 | if (cb) cb(err);
66 | };
67 | iface.RegisterAgent(this.path, 'KeyboardOnly');
68 | });
69 | }
70 |
71 | BluezAgent.prototype.setDefault = function(cb) {
72 | if (!this.iface) {
73 | if (cb) cb('Agent was not registered yet');
74 | return;
75 | }
76 |
77 | this.iface.RequestDefaultAgent['finish'] = () => {
78 | this.debug('Set agent as default');
79 | if (cb) cb();
80 | };
81 | this.iface.RequestDefaultAgent['error'] = (err) => {
82 | this.debug('Failed settings as default agent: ' + err);
83 | if (cb) cb(err);
84 | };
85 | this.iface.RequestDefaultAgent(this.path);
86 | }
87 |
88 | BluezAgent.prototype.setPasskeyHandler = function(func) {
89 | this.passkeyHandler = func;
90 | }
91 |
--------------------------------------------------------------------------------
/BluezCharacteristic.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events').EventEmitter;
2 | const util = require('util');
3 | const _ = require('underscore');
4 | const bluezDBus = require('./BluezDBus');
5 |
6 | module.exports = BluezCharacteristic;
7 |
8 | function BluezCharacteristic(path) {
9 | this.debug = require('debug')('Bluez:Characteristic:' + path);
10 | this.path = path;
11 | this.debug('New characteristic');
12 | }
13 |
14 | util.inherits(BluezCharacteristic, EventEmitter);
15 |
16 | BluezCharacteristic.prototype.init = function(cb) {
17 | /* Get GattCharacteristic1 interface */
18 | bluezDBus.getInterface(this.path, 'org.bluez.GattCharacteristic1',
19 | (err, iface) => {
20 | if (err) {
21 | this.debug('Failed getting GattCharacteristic1 interface:' + err);
22 | if (cb) cb(err);
23 | return;
24 | }
25 |
26 | /* Save this interface */
27 | this.iface = iface;
28 |
29 | /* Get Protperties interface */
30 | this.props = bluezDBus.getProperties(this.path,
31 | 'org.bluez.GattCharacteristic1',
32 | this._characteristicPropertiesUpdate.bind(this), /* Property changed */
33 | (err) => { /* All propertires were resolved */
34 | if (err) {
35 | this.debug('Failed getting all properties:' + err);
36 | if (cb) cb(err);
37 | return;
38 | }
39 |
40 | /* The new characteristic is ready */
41 | if (cb) cb();
42 | });
43 | }
44 | );
45 |
46 | /* Save the event handler so we can remove it later */
47 | this.ifaceEvents = bluezDBus.onInterfaces(null,
48 | this._interfaceRemoved.bind(this));
49 | }
50 |
51 | BluezCharacteristic.prototype.toString = function() {
52 | return '[Object BluezCharacteristic (' + this.path + ')]';
53 | }
54 |
55 | BluezCharacteristic.prototype._characteristicPropertiesUpdate = function(key, value) {
56 | if (_.isEqual(this[key], value))
57 | return;
58 |
59 | this.debug(key + ' changed from ' + this[key] + ' to ' + value);
60 | this[key] = value;
61 | this.emit('propertyChanged', key, value);
62 | }
63 |
64 | BluezCharacteristic.prototype._interfaceRemoved = function(path, objects) {
65 | /* We're only interested in ourselves */
66 | if (this.path !== path)
67 | return;
68 |
69 | this.debug('Removed characteristic');
70 | this.emit('removed');
71 | this.removeAllListeners();
72 | this.props.close();
73 | this.ifaceEvents.close();
74 | }
75 |
76 | BluezCharacteristic.prototype.Read = function(cb) {
77 | this.iface.ReadValue['finish'] = (value) => {
78 | this.debug('Read: ' + value);
79 | if (cb) cb(null, value);
80 | };
81 | this.iface.ReadValue['error'] = (err) => {
82 | this.debug('Failed reading: ' + err);
83 | if (cb) cb(err);
84 | };
85 | this.iface.ReadValue({});
86 | }
87 |
88 | BluezCharacteristic.prototype.Write = function(value, cb) {
89 | this.iface.WriteValue['finish'] = () => {
90 | this.debug('Wrote value: ' + value);
91 | if (cb) cb();
92 | };
93 | this.iface.WriteValue['error'] = (err) => {
94 | this.debug('Failed writing: ' + err);
95 | if (cb) cb(err);
96 | };
97 | this.iface.WriteValue(value, {});
98 | }
99 |
100 | BluezCharacteristic.prototype.NotifyStart = function(cb) {
101 | this.iface.StartNotify['finish'] = () => {
102 | this.debug('Listening on notifications');
103 | if (cb) cb();
104 | };
105 | this.iface.StartNotify['error'] = (err) => {
106 | this.debug('Failed listening on notifications: ' + err);
107 | if (cb) cb(err);
108 | };
109 | this.iface.StartNotify();
110 | }
111 |
112 | BluezCharacteristic.prototype.NotifyStop = function(cb) {
113 | this.iface.StopNotify['finish'] = () => {
114 | this.debug('Stopped listening on notifications');
115 | if (cb) cb();
116 | };
117 | this.iface.StopNotify['error'] = (err) => {
118 | this.debug('Failed stopping listening on notifications: ' + err);
119 | if (cb) cb(err);
120 | };
121 | this.iface.StopNotify();
122 | }
123 |
--------------------------------------------------------------------------------
/BluezDBus.js:
--------------------------------------------------------------------------------
1 | const events = require('events')
2 | const debug = require('debug')('BluezDBus');
3 | const DBus = require('dbus');
4 | const dbus = new DBus();
5 | const bus = dbus.getBus('system');
6 |
7 | var bluezDBus = new events.EventEmitter();
8 |
9 | var objectManagerInterface = null;
10 |
11 | bluezDBus.getInterface = function(path, iface, cb) {
12 | bus.getInterface('org.bluez', path, iface, cb);
13 | }
14 |
15 | function _notifyPropertyChange(func, properties) {
16 | if (!func)
17 | return;
18 |
19 | Object.keys(properties).forEach((key) => func(key, properties[key]));
20 | }
21 |
22 | bluezDBus.setMaxListeners(Infinity);
23 | bluezDBus.getProperties = function(path, interfaceName, propertyChangedCb, resolvedCb) {
24 | var ctx = {
25 | close: function() {
26 | this.iface.removeListener('PropertiesChanged', this.cb);
27 | }
28 | }
29 | bluezDBus.getInterface(path, 'org.freedesktop.DBus.Properties',
30 | (err, iface) => {
31 | if (err) {
32 | debug('Failed getting properties for ' + path + ': ' + err);
33 | return;
34 | }
35 |
36 | /* Save interface an callback so we can unregister later */
37 | ctx.iface = iface;
38 | ctx.cb = (_interfaceName, properties) => {
39 | if (_interfaceName !== interfaceName)
40 | return;
41 |
42 | _notifyPropertyChange(propertyChangedCb, properties);
43 | };
44 | /* Listen on changes */
45 | iface.on('PropertiesChanged', ctx.cb);
46 |
47 | /* Get all properties */
48 | iface.GetAll['finish'] = (properties) => {
49 | _notifyPropertyChange(propertyChangedCb, properties);
50 |
51 | if (resolvedCb)
52 | resolvedCb();
53 | };
54 | iface.GetAll['error'] = resolvedCb;
55 | iface.GetAll(interfaceName);
56 | }
57 | );
58 |
59 | return ctx;
60 | }
61 |
62 | bluezDBus.getAllObjects = function(cb) {
63 | if (objectManagerInterface === null) {
64 | /* ObjectManager isn't available yet, try again later */
65 | setTimeout(() => bluezDBus.getAllObjects(cb), 100);
66 | return;
67 | }
68 |
69 | /* Set callback functions */
70 | objectManagerInterface.GetManagedObjects['finish'] = (objects) => {
71 | cb(undefined, objects);
72 | };
73 | objectManagerInterface.GetManagedObjects['error'] = cb;
74 |
75 | /* Initiate call */
76 | objectManagerInterface.GetManagedObjects();
77 | }
78 |
79 | bluezDBus.onInterfaces = function(interfaceAddedCb, interfaceRemovedCb) {
80 | if (interfaceAddedCb) this.on('interfaceAdded', interfaceAddedCb);
81 | if (interfaceRemovedCb) this.on('interfaceRemoved', interfaceRemovedCb);
82 |
83 | return {
84 | _addedCb: interfaceAddedCb,
85 | _removedCb: interfaceRemovedCb,
86 | _bluezDBus: this,
87 | close: function() {
88 | if (this._addedCb)
89 | this._bluezDBus.removeListener('interfaceAdded', this._addedCb);
90 | if (this._removedCb)
91 | this._bluezDBus.removeListener('interfaceRemoved', this._removedCb);
92 | }
93 | };
94 | }
95 |
96 | /* Get ObjectManager and register events */
97 | bluezDBus.getInterface('/', 'org.freedesktop.DBus.ObjectManager',
98 | (err, iface) => {
99 | if (err)
100 | throw 'Failed getting the ObjectManager interface: ' + err;
101 |
102 | /* Save interface */
103 | objectManagerInterface = iface;
104 |
105 | /* Listen on object changes and notify */
106 | objectManagerInterface.on('InterfacesAdded', (path, objects) => {
107 | bluezDBus.emit('interfaceAdded', path, objects);
108 | });
109 | objectManagerInterface.on('InterfacesRemoved', (path, objects) => {
110 | bluezDBus.emit('interfaceRemoved', path, objects);
111 | });
112 | }
113 | );
114 |
115 | module.exports = bluezDBus;
116 |
--------------------------------------------------------------------------------
/BluezDevice.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events').EventEmitter;
2 | const util = require('util');
3 | const _ = require('underscore');
4 | const bluezDBus = require('./BluezDBus');
5 | const BluezService = require('./BluezService');
6 |
7 | module.exports = BluezDevice;
8 |
9 | function BluezDevice(path) {
10 | this.debug = require('debug')('Bluez:Device:' + path);
11 | this.path = path;
12 | this.debug('New device');
13 | }
14 |
15 | util.inherits(BluezDevice, EventEmitter);
16 |
17 | BluezDevice.prototype.init = function(cb) {
18 | /* Get Device1 interface */
19 | bluezDBus.getInterface(this.path, 'org.bluez.Device1', (err, iface) => {
20 | if (err) {
21 | this.debug('Failed getting Device1 interface:' + err);
22 | if (cb) cb(err);
23 | return;
24 | }
25 |
26 | /* Save this interface */
27 | this.iface = iface;
28 |
29 | /* Get Protperties interface */
30 | this.props = bluezDBus.getProperties(this.path, 'org.bluez.Device1',
31 | this._devicePropertiesUpdate.bind(this), /* Property changed */
32 | (err) => { /* All propertires were resolved */
33 | if (err) {
34 | this.debug('Failed getting all properties:' + err);
35 | if (cb) cb(err);
36 | return;
37 | }
38 |
39 | /* The new device is ready */
40 | if (cb) cb();
41 | });
42 | });
43 |
44 | /* Save the event handler so we can remove it later */
45 | this.ifaceEvents = bluezDBus.onInterfaces(this._interfaceAdded.bind(this),
46 | this._interfaceRemoved.bind(this));
47 | }
48 |
49 | BluezDevice.prototype.toString = function() {
50 | return '[Object BluezDevice (' + this.path + ')]';
51 | }
52 |
53 | BluezDevice.prototype._isOwnService = function(path, objects) {
54 | /* If the services were not resolved yet, we don't care about cached ones */
55 | if (!this.ServicesResolved)
56 | return false;
57 |
58 | /* Make sure this services was discovered on this device */
59 | if (!path.startsWith(this.path))
60 | return false;
61 |
62 | /* We're only interested in services */
63 | if (objects['org.bluez.GattService1'] === undefined)
64 | return false;
65 |
66 | return true;
67 | }
68 |
69 | BluezDevice.prototype._onServicesResolvedChanged = function(servicesResolved) {
70 | if (!servicesResolved)
71 | return;
72 |
73 | /* The device is resolved, now let's check if there are any relevant objects
74 | * (services) */
75 | bluezDBus.getAllObjects((err, objects) => {
76 | if (err)
77 | throw 'Failed getting all objects';
78 |
79 | Object.keys(objects).forEach((key) => {
80 | this._interfaceAdded(key, objects[key]);
81 | });
82 | });
83 | }
84 |
85 | BluezDevice.prototype._devicePropertiesUpdate = function(key, value) {
86 | if (_.isEqual(this[key], value))
87 | return;
88 |
89 | this.debug(key + ' changed from ' + this[key] + ' to ' + value);
90 | this[key] = value;
91 |
92 | if (key == 'ServicesResolved')
93 | this._onServicesResolvedChanged(value);
94 |
95 | this.emit('propertyChanged', key, value);
96 | }
97 |
98 | BluezDevice.prototype._interfaceAdded = function(path, objects) {
99 | /* We're only interested in services under this device */
100 | if (!this._isOwnService(path, objects))
101 | return;
102 |
103 | this.debug('A service was added: ' + path);
104 | var service = new BluezService(path);
105 | service.init((err) => {
106 | if (err) {
107 | this.debug('Failed initializing new service ' + path + ': ' + err);
108 | return;
109 | }
110 | this.emit('service', service);
111 | });
112 | }
113 |
114 | BluezDevice.prototype._interfaceRemoved = function(path, objects) {
115 | /* We're only interested in ourselves */
116 | if (this.path !== path)
117 | return;
118 |
119 | this.debug('Removed device');
120 | this.emit('removed');
121 | this.removeAllListeners();
122 | this.props.close();
123 | this.ifaceEvents.close();
124 | }
125 |
126 | BluezDevice.prototype.Connect = function(cb) {
127 | this.iface.Connect['finish'] = () => {
128 | this.debug('Connected');
129 | if (cb) cb();
130 | };
131 | this.iface.Connect['error'] = (err) => {
132 | this.debug('Failed connecting: ' + err);
133 | if (cb) cb(err);
134 | };
135 | this.iface.Connect();
136 | }
137 |
138 | BluezDevice.prototype.Disconnect = function(cb) {
139 | this.iface.Disconnect['finish'] = () => {
140 | this.debug('Disconnected');
141 | if (cb) cb();
142 | };
143 | this.iface.Disconnect['error'] = (err) => {
144 | this.debug('Failed disconnecting: ' + err);
145 | if (cb) cb(err);
146 | };
147 | this.iface.Disconnect();
148 | }
149 |
--------------------------------------------------------------------------------
/BluezService.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events').EventEmitter;
2 | const util = require('util');
3 | const _ = require('underscore');
4 | const bluezDBus = require('./BluezDBus');
5 | const BluezCharacteristic = require('./BluezCharacteristic');
6 |
7 | module.exports = BluezService;
8 |
9 | function BluezService(path) {
10 | this.debug = require('debug')('Bluez:Service:' + path);
11 | this.path = path;
12 | this.debug('New service');
13 | }
14 |
15 | util.inherits(BluezService, EventEmitter);
16 |
17 | BluezService.prototype.init = function(cb) {
18 | /* Get GattService1 interface */
19 | bluezDBus.getInterface(this.path, 'org.bluez.GattService1', (err, iface) => {
20 | if (err) {
21 | this.debug('Failed getting GattService1 interface:' + err);
22 | if (cb) cb(err);
23 | return;
24 | }
25 |
26 | /* Save this interface */
27 | this.iface = iface;
28 |
29 | /* Get Protperties interface */
30 | this.props = bluezDBus.getProperties(this.path, 'org.bluez.GattService1',
31 | this._servicePropertiesUpdate.bind(this), /* Property changed */
32 | (err) => { /* All propertires were resolved */
33 | if (err) {
34 | this.debug('Failed getting all properties:' + err);
35 | if (cb) cb(err);
36 | return;
37 | }
38 |
39 | /* At this point, the device is connected and the services are already
40 | * resolved, just look for the relevant characteristics */
41 | bluezDBus.getAllObjects((err, objects) => {
42 | if (err)
43 | throw 'Failed getting all objects';
44 |
45 | Object.keys(objects).forEach((key) => {
46 | this._interfaceAdded(key, objects[key]);
47 | });
48 |
49 | /* The new service is ready */
50 | if (cb) cb();
51 | });
52 | });
53 | });
54 |
55 | /* Save the event handler so we can remove it later */
56 | this.ifaceEvents = bluezDBus.onInterfaces(this._interfaceAdded.bind(this),
57 | this._interfaceRemoved.bind(this));
58 | }
59 |
60 | BluezService.prototype.toString = function() {
61 | return '[Object BluezService (' + this.path + ')]';
62 | }
63 |
64 | BluezService.prototype._isOwnCharacteristic = function(path, objects) {
65 | /* Make sure this characteristic was discovered on this service */
66 | if (!path.startsWith(this.path))
67 | return false;
68 |
69 | /* We're only interested in services */
70 | if (objects['org.bluez.GattCharacteristic1'] === undefined)
71 | return false;
72 |
73 | return true;
74 | }
75 |
76 | BluezService.prototype._servicePropertiesUpdate = function(key, value) {
77 | if (_.isEqual(this[key], value))
78 | return;
79 |
80 | this.debug(key + ' changed from ' + this[key] + ' to ' + value);
81 | this[key] = value;
82 | this.emit('propertyChanged', key, value);
83 | }
84 |
85 | BluezService.prototype._interfaceAdded = function(path, objects) {
86 | /* We're only interested in characteristics under this service */
87 | if (!this._isOwnCharacteristic(path, objects))
88 | return;
89 |
90 | this.debug('A characteristic was added: ' + path);
91 | var characteristic = new BluezCharacteristic(path);
92 | characteristic.init((err) => {
93 | if (err) {
94 | this.debug('Failed initializing new characteristic ' + path + ': ' + err);
95 | return;
96 | }
97 | this.emit('characteristic', characteristic);
98 | });
99 | }
100 |
101 | BluezService.prototype._interfaceRemoved = function(path, objects) {
102 | /* We're only interested in ourselves */
103 | if (this.path !== path)
104 | return;
105 |
106 | this.debug('Removed service');
107 | this.emit('removed');
108 | this.removeAllListeners();
109 | this.props.close();
110 | this.ifaceEvents.close();
111 | }
112 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Assaf Inbal
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BLE2MQTT
2 |
3 | **Depreciation Notice** - This project is no longer maintained in favor of the
4 | [ESP32](https://github.com/shmuelzon/esp32-ble2mqtt) variant. Pull requests and
5 | questions are still welcome.
6 |
7 | This project aims to be a BLE to MQTT bridge, i.e. expose BLE GATT
8 | characteristics as MQTT topics for bidirectional communication. It relies on the
9 | BlueZ DBus API and as such is supported on Linux only.
10 |
11 | For example, if a device with a MAC address of `A0:E6:F8:50:72:53` exposes the
12 | [0000180f-0000-1000-8000-00805f9b34fb service](https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.battery_service.xml)
13 | (Battery Service) which includes the
14 | [00002a19-0000-1000-8000-00805f9b34fb characteristic](https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.battery_level.xml)
15 | (Battery Level), the `A0:E6:F8:50:72:53/BatteryService/BatteryLevel`
16 | MQTT topic is published with a value representing the battery level.
17 |
18 | In order to set a GATT value, publish a message to a writable characteristic
19 | using the above format suffixed with `/Set`. Note that values are byte arrays so
20 | writing a 64-bit value would look like `10,231,32,24`.
21 |
22 | ## Configuration
23 |
24 | While the basic configuration file provided in the repository
25 | ([config.json](https://github.com/shmuelzon/ble2mqtt/blob/master/config.json))
26 | should be enough for most users, it is possible to tweak it a bit to fit one's
27 | specific needs.
28 |
29 | The `mqtt` section below includes the following entries:
30 | ```json
31 | {
32 | "mqtt": {
33 | "server": {
34 | "host": "127.0.0.1",
35 | "port": 1883
36 | },
37 | "publish": {
38 | "retain": true
39 | },
40 | "topics" :{
41 | "device_name": "Address",
42 | "set_suffix": "/Set"
43 | }
44 | }
45 | }
46 | ```
47 | * `server` - define which MQTT broker the application should connect to
48 | * `publish` - configuration for publishing topics. This object is passed as-is
49 | to the [mqtt.publish()](https://github.com/mqttjs/MQTT.js#publish) method
50 | * `topics`
51 | * `device name` - define which attribute of a device (as exposed by Bluez)
52 | should be used to identify it in the MQTT topic
53 | * `set_suffix` - Which suffix should be added to the MQTT value topic in order
54 | to write a new value to the characteristic. Set this to an empty string if
55 | you wish to use the same topic for reading and writing
56 |
57 | The `ble` section of the configuration file includes the following default
58 | configuration:
59 | ```json
60 | {
61 | "ble": {
62 | "services": { },
63 | "characteristics": { }
64 | }
65 | }
66 | ```
67 | * `services` - add an additional service or override an existing definition to
68 | the ones grabbed automatically on first run from http://www.bluetooth.org.
69 | Each service can include a `name` field which will be used in the MQTT topic
70 | instead of its UUID. For example:
71 |
72 | ```json
73 | "services": {
74 | "00002f00-0000-1000-8000-00805f9b34fb": {
75 | "name": "Relay Service"
76 | },
77 | }
78 | ```
79 | * `characteristics` - add an additional characteristic or override an existing
80 | definition to the ones grabbed automatically on first run from
81 | http://www.bluetooth.org. Each characteristic can include a `name` field which
82 | will be used in the MQTT topic instead of its UUID, a `types` array defining
83 | how to parse the byte array reflecting the characteristic's value and a `poll`
84 | value (in seconds) for the application to poll the BLE device for a new value.
85 | For example:
86 |
87 | ```json
88 | "characteristics": {
89 | "00002a19-0000-1000-8000-00805f9b34fb": {
90 | "//": "Poll the battery level characteristic every day",
91 | "poll": 86400
92 | },
93 | "00002f01-0000-1000-8000-00805f9b34fb": {
94 | "name": "Relay State",
95 | "types": [
96 | "boolean"
97 | ]
98 | }
99 | }
100 | ```
101 | * `whitelist`/`blacklist` - An array of strings or regular expressions used to
102 | match MAC addresses of devices. If `whitelist` is used, only devices with a
103 | MAC address matching one of the entries will be connected while if `blacklist`
104 | is used, only devices that do not match any entry will be connected to.
105 |
106 | ```json
107 | "whitelist": [
108 | "A0:E6:F8:.*"
109 | ]
110 | ```
111 |
112 | * `passkeys` - An object containing the passkey (number 000000~999999) used for
113 | out-of-band authorization. Each entry is the MAC address of the BLE device and
114 | the value is the passkey to use.
115 |
116 | ```json
117 | "passkeys": {
118 | "B0:B4:48:D3:63:98": 123456
119 | }
120 | ```
121 |
122 | ## Installation
123 |
124 | This app requires node version >= 4.3.2 (need support for arrow functions) as
125 | well as a fairly recent version of Bluez (>= 5.40).
126 |
127 | > Note that you can probably point your apt sources to stretch/testing to get
128 | > newer versions of these packages. I, personally, haven't tried that yet
129 |
130 | ### Bluez
131 |
132 | My personal setup is a Raspberry Pi 3 utilizing its built-in Bluetooth radio. I
133 | needed to build a newer version of Bluez and needed it to be a Debian package
134 | since a different package (pi-bluetooth, which creates the HCI device) depends
135 | on it. To overcome this, I ran the following:
136 |
137 | ```bash
138 | # Get dependencies
139 | sudo apt-get update
140 | sudo apt-get install -y libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev \
141 | libical-dev libreadline-dev checkinstall
142 |
143 | # Compile + Install Bluez 5.45
144 | mkdir -p ~/Downloads
145 | wget -O ~/Downloads/bluez-5.45.tar.xz http://www.kernel.org/pub/linux/bluetooth/bluez-5.45.tar.xz
146 | mkdir -p ~/code
147 | cd ~/code
148 | tar -xvf ~/Downloads/bluez-5.45.tar.xz
149 | cd bluez-5.45
150 |
151 | # Allow tabs to be tabs (for patches)
152 | bind '\C-i:self-insert'
153 |
154 | patch -p1 << EOF
155 | --- a/tools/hciattach.c
156 | +++ b/tools/hciattach.c
157 | @@ -1236,7 +1236,7 @@
158 | {
159 | struct uart_t *u = NULL;
160 | int detach, printpid, raw, opt, i, n, ld, err;
161 | - int to = 10;
162 | + int to = 30;
163 | int init_speed = 0;
164 | int send_break = 0;
165 | pid_t pid;
166 | --- a/tools/hciattach_bcm43xx.c
167 | +++ b/tools/hciattach_bcm43xx.c
168 | @@ -43,7 +43,7 @@
169 | #include "hciattach.h"
170 |
171 | #ifndef FIRMWARE_DIR
172 | -#define FIRMWARE_DIR "/etc/firmware"
173 | +#define FIRMWARE_DIR "/lib/firmware"
174 | #endif
175 |
176 | #define FW_EXT ".hcd"
177 | @@ -366,11 +366,8 @@
178 | return -1;
179 |
180 | if (bcm43xx_locate_patch(FIRMWARE_DIR, chip_name, fw_path)) {
181 | - fprintf(stderr, "Patch not found, continue anyway\n");
182 | + fprintf(stderr, "Patch not found for %s, continue anyway\n", chip_name);
183 | } else {
184 | - if (bcm43xx_set_speed(fd, ti, speed))
185 | - return -1;
186 | -
187 | if (bcm43xx_load_firmware(fd, fw_path))
188 | return -1;
189 |
190 | @@ -380,6 +377,7 @@
191 | return -1;
192 | }
193 |
194 | + sleep(1);
195 | if (bcm43xx_reset(fd))
196 | return -1;
197 | }
198 | --- a/src/bluetooth.conf
199 | +++ b/src/bluetooth.conf
200 | @@ -34,6 +34,10 @@
201 |