@stoprocent/noble
Version:
A Node.js BLE (Bluetooth Low Energy) central library.
808 lines (719 loc) • 29.3 kB
JavaScript
const { EventEmitter } = require('events');
const debug = require('debug')('noble-dbus');
const {
normalizeUuid,
expandUuid,
addressToId,
devicePathToAddress,
deviceIdFromPath,
devicePathFromAddress
} = require('./uuid');
const BLUEZ_SERVICE = 'org.bluez';
const ROOT_PATH = '/';
const ADAPTER_IFACE = 'org.bluez.Adapter1';
const DEVICE_IFACE = 'org.bluez.Device1';
const GATT_SERVICE_IFACE = 'org.bluez.GattService1';
const GATT_CHAR_IFACE = 'org.bluez.GattCharacteristic1';
const GATT_DESC_IFACE = 'org.bluez.GattDescriptor1';
const PROPS_IFACE = 'org.freedesktop.DBus.Properties';
const OBJECT_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager';
const FLAG_TO_PROPERTY = {
broadcast: 'broadcast',
read: 'read',
'write-without-response': 'writeWithoutResponse',
write: 'write',
notify: 'notify',
indicate: 'indicate',
'authenticated-signed-writes': 'authenticatedSignedWrites',
'reliable-write': 'extendedProperties',
'writable-auxiliaries': 'extendedProperties'
};
function loadDbus () {
try {
// dbus-next is an optional peer of this Linux-only backend; the host
// project installs it explicitly. eslint-plugin-node would otherwise
// flag the missing module on platforms where it is not present.
// eslint-disable-next-line node/no-missing-require
return require('dbus-next');
} catch (err) {
const wrapped = new Error(
'noble dbus backend requires the "dbus-next" package. ' +
'Install it with: npm install dbus-next'
);
wrapped.cause = err;
throw wrapped;
}
}
function normalizeId (id) {
// BlueZ emits MACs uppercase; noble's id form is colon-stripped lowercase.
// Accept either, plus mixed case, so external callers don't have to care.
if (id == null) return id;
return String(id).replace(/:/g, '').toLowerCase();
}
function unwrapVariant (variant) {
if (variant && typeof variant === 'object' && 'value' in variant && 'signature' in variant) {
return variant.value;
}
return variant;
}
function unwrapDict (dict) {
const out = {};
if (!dict) return out;
for (const key of Object.keys(dict)) {
out[key] = unwrapVariant(dict[key]);
}
return out;
}
function flagsToProperties (flags) {
const set = new Set();
for (const flag of flags || []) {
const mapped = FLAG_TO_PROPERTY[flag];
if (mapped) set.add(mapped);
}
return Array.from(set);
}
function buildAdvertisement (deviceProps) {
const advertisement = {
localName: undefined,
txPowerLevel: undefined,
manufacturerData: undefined,
serviceData: [],
serviceUuids: [],
serviceSolicitationUuids: []
};
if (typeof deviceProps.Name === 'string') {
advertisement.localName = deviceProps.Name;
} else if (typeof deviceProps.Alias === 'string') {
advertisement.localName = deviceProps.Alias;
}
if (typeof deviceProps.TxPower === 'number') {
advertisement.txPowerLevel = deviceProps.TxPower;
}
if (Array.isArray(deviceProps.UUIDs)) {
advertisement.serviceUuids = deviceProps.UUIDs.map(normalizeUuid);
}
if (deviceProps.ManufacturerData && typeof deviceProps.ManufacturerData === 'object') {
const entries = Object.entries(deviceProps.ManufacturerData);
if (entries.length > 0) {
const buffers = [];
for (const [companyId, rawPayload] of entries) {
const payload = unwrapVariant(rawPayload);
const id = Number(companyId) & 0xffff;
const header = Buffer.from([id & 0xff, (id >> 8) & 0xff]);
const data = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
buffers.push(Buffer.concat([header, data]));
}
advertisement.manufacturerData = buffers.length === 1 ? buffers[0] : Buffer.concat(buffers);
}
}
if (deviceProps.ServiceData && typeof deviceProps.ServiceData === 'object') {
for (const [uuid, rawPayload] of Object.entries(deviceProps.ServiceData)) {
const payload = unwrapVariant(rawPayload);
advertisement.serviceData.push({
uuid: normalizeUuid(uuid),
data: Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
});
}
}
return advertisement;
}
class DbusBindings extends EventEmitter {
constructor (options = {}) {
super();
this._options = options || {};
this._dbus = null;
this._bus = null;
this._rootProxy = null;
this._objectManager = null;
this._adapterPath = null;
this._adapterProxy = null;
this._adapterIface = null;
this._adapterProps = null;
this._adapterAddress = 'unknown';
this._state = 'unknown';
this._isScanning = false;
// Object tree mirror: path -> { iface: props }
this._objects = new Map();
// Discovery filter for service uuid filtering
this._scanServiceUuids = [];
// Per-device live state
// id -> { path, address, addressType, connectable, scannable, rssi, advertisement, proxy, propsListener, connectPromise, servicesResolved }
this._devices = new Map();
// Per-characteristic notify listener: path -> { iface, listener }
this._charPropsListeners = new Map();
this._onInterfacesAddedBound = this._onInterfacesAdded.bind(this);
this._onInterfacesRemovedBound = this._onInterfacesRemoved.bind(this);
this._onAdapterPropsBound = this._onAdapterProperties.bind(this);
}
start () {
this._dbus = loadDbus();
this._bus = this._dbus.systemBus();
this._init().catch(err => {
debug('init failed: %s', err && err.stack);
this.emit('warning', `dbus init failed: ${err.message}`);
this._state = 'unsupported';
this.emit('stateChange', 'unsupported');
});
}
async _init () {
this._rootProxy = await this._bus.getProxyObject(BLUEZ_SERVICE, ROOT_PATH);
this._objectManager = this._rootProxy.getInterface(OBJECT_MANAGER_IFACE);
this._objectManager.on('InterfacesAdded', this._onInterfacesAddedBound);
this._objectManager.on('InterfacesRemoved', this._onInterfacesRemovedBound);
const managed = await this._objectManager.GetManagedObjects();
for (const [path, ifaces] of Object.entries(managed)) {
const unwrapped = {};
for (const [iface, props] of Object.entries(ifaces)) {
unwrapped[iface] = unwrapDict(props);
}
this._objects.set(path, unwrapped);
}
const adapterPath = this._pickAdapterPath();
if (!adapterPath) {
throw new Error('No BlueZ adapter found (is bluetoothd running?)');
}
this._adapterPath = adapterPath;
const adapterProxy = await this._bus.getProxyObject(BLUEZ_SERVICE, adapterPath);
this._adapterProxy = adapterProxy;
this._adapterIface = adapterProxy.getInterface(ADAPTER_IFACE);
this._adapterProps = adapterProxy.getInterface(PROPS_IFACE);
this._adapterProps.on('PropertiesChanged', this._onAdapterPropsBound);
const adapterProps = this._objects.get(adapterPath)[ADAPTER_IFACE] || {};
if (typeof adapterProps.Address === 'string') {
this._adapterAddress = adapterProps.Address;
this.emit('addressChange', adapterProps.Address);
}
const powered = !!adapterProps.Powered;
this._setState(powered ? 'poweredOn' : 'poweredOff');
// Surface devices that already exist in BlueZ's cache as discovery events.
for (const [path, ifaces] of this._objects) {
if (ifaces[DEVICE_IFACE] && this._isUnderAdapter(path)) {
this._handleDeviceProps(path, ifaces[DEVICE_IFACE]);
}
}
}
_pickAdapterPath () {
const requested = this._options.adapterId
|| (this._options.hciDeviceId != null ? `hci${this._options.hciDeviceId}` : null);
let firstMatch = null;
for (const [path, ifaces] of this._objects) {
if (!ifaces[ADAPTER_IFACE]) continue;
if (!firstMatch) firstMatch = path;
if (requested && path.endsWith(`/${requested}`)) return path;
}
return firstMatch;
}
_isUnderAdapter (path) {
return this._adapterPath && path.startsWith(`${this._adapterPath}/`);
}
_setState (state) {
if (this._state === state) return;
this._state = state;
this.emit('stateChange', state);
}
stop () {
if (this._objectManager) {
this._objectManager.off('InterfacesAdded', this._onInterfacesAddedBound);
this._objectManager.off('InterfacesRemoved', this._onInterfacesRemovedBound);
}
if (this._adapterProps) {
this._adapterProps.off('PropertiesChanged', this._onAdapterPropsBound);
}
for (const device of this._devices.values()) {
if (device.proxy && device.propsListener) {
const props = device.proxy.getInterface(PROPS_IFACE);
props.off('PropertiesChanged', device.propsListener);
}
}
for (const entry of this._charPropsListeners.values()) {
entry.props.off('PropertiesChanged', entry.listener);
}
this._charPropsListeners.clear();
if (this._isScanning && this._adapterIface) {
this._adapterIface.StopDiscovery().catch(() => {});
}
if (this._bus && typeof this._bus.disconnect === 'function') {
try { this._bus.disconnect(); } catch (_) { /* ignore */ }
}
}
setScanParameters (_interval, _window) {
this.emit('warning', 'setScanParameters is not supported on the dbus backend (BlueZ controls scan parameters)');
this.emit('scanParametersSet');
}
setAddress (_address) {
this.emit('warning', 'setAddress is not supported on the dbus backend');
}
startScanning (serviceUuids, allowDuplicates) {
this._scanServiceUuids = (serviceUuids || []).map(normalizeUuid);
this._startScanning(allowDuplicates).catch(err => {
this.emit('warning', `startScanning failed: ${err.message}`);
});
}
async _startScanning (allowDuplicates) {
if (!this._adapterIface) {
throw new Error('adapter not initialized');
}
const { Variant } = this._dbus;
const filter = {
Transport: new Variant('s', 'le'),
DuplicateData: new Variant('b', !!allowDuplicates)
};
if (this._scanServiceUuids.length > 0) {
filter.UUIDs = new Variant('as', this._scanServiceUuids.map(expandUuid));
}
try {
await this._adapterIface.SetDiscoveryFilter(filter);
} catch (err) {
debug('SetDiscoveryFilter failed: %s', err.message);
}
if (!this._isScanning) {
await this._adapterIface.StartDiscovery();
this._isScanning = true;
}
this.emit('scanStart', !!allowDuplicates);
}
stopScanning () {
this._stopScanning().catch(err => {
this.emit('warning', `stopScanning failed: ${err.message}`);
this.emit('scanStop');
});
}
async _stopScanning () {
if (this._isScanning && this._adapterIface) {
try {
await this._adapterIface.StopDiscovery();
} catch (err) {
debug('StopDiscovery failed: %s', err.message);
}
this._isScanning = false;
}
this.emit('scanStop');
}
// ---- ObjectManager + property change handlers ----
_onInterfacesAdded (path, ifaces) {
const unwrapped = {};
for (const [iface, props] of Object.entries(ifaces)) {
unwrapped[iface] = unwrapDict(props);
}
const existing = this._objects.get(path) || {};
this._objects.set(path, Object.assign(existing, unwrapped));
if (unwrapped[DEVICE_IFACE] && this._isUnderAdapter(path)) {
this._handleDeviceProps(path, unwrapped[DEVICE_IFACE]);
}
// Trigger services-resolved processing if a device just gained ServicesResolved
if (unwrapped[GATT_SERVICE_IFACE] || unwrapped[GATT_CHAR_IFACE] || unwrapped[GATT_DESC_IFACE]) {
// No direct emit; clients call discoverServices/Characteristics/Descriptors.
}
}
_onInterfacesRemoved (path, ifaces) {
const stored = this._objects.get(path);
if (stored) {
for (const iface of ifaces) delete stored[iface];
if (Object.keys(stored).length === 0) {
this._objects.delete(path);
}
}
if (ifaces.includes(DEVICE_IFACE)) {
const id = deviceIdFromPath(path);
if (id && this._devices.has(id)) {
this._onDeviceDisconnected(id, 'removed');
}
}
}
_onAdapterProperties (iface, changed) {
if (iface !== ADAPTER_IFACE) return;
const props = unwrapDict(changed);
if ('Powered' in props) {
this._setState(props.Powered ? 'poweredOn' : 'poweredOff');
}
if (typeof props.Address === 'string') {
this._adapterAddress = props.Address;
this.emit('addressChange', props.Address);
}
const stored = this._objects.get(this._adapterPath) || {};
stored[ADAPTER_IFACE] = Object.assign(stored[ADAPTER_IFACE] || {}, props);
this._objects.set(this._adapterPath, stored);
}
_handleDeviceProps (path, props) {
const address = props.Address || devicePathToAddress(path);
if (!address) return;
const id = addressToId(address);
let device = this._devices.get(id);
if (!device) {
device = {
path,
address,
addressType: props.AddressType || 'unknown',
connectable: true,
scannable: false,
rssi: typeof props.RSSI === 'number' ? props.RSSI : 0,
advertisement: buildAdvertisement(props),
proxy: null,
propsListener: null,
servicesResolved: !!props.ServicesResolved,
connectPromise: null
};
this._devices.set(id, device);
} else {
device.path = path;
device.address = address;
if (props.AddressType) device.addressType = props.AddressType;
if (typeof props.RSSI === 'number') device.rssi = props.RSSI;
Object.assign(device.advertisement, buildAdvertisement(props));
}
this.emit(
'discover',
id,
device.address,
device.addressType,
device.connectable,
device.advertisement,
device.rssi,
device.scannable
);
}
// ---- Per-device proxy + property listening ----
async _ensureDeviceProxy (id) {
const device = this._devices.get(id);
if (!device) throw new Error(`unknown peripheral ${id}`);
if (device.proxy) return device;
const path = device.path || devicePathFromAddress(this._adapterPath, device.address);
device.path = path;
device.proxy = await this._bus.getProxyObject(BLUEZ_SERVICE, path);
const props = device.proxy.getInterface(PROPS_IFACE);
device.propsListener = (iface, changed) => {
if (iface !== DEVICE_IFACE) return;
const c = unwrapDict(changed);
if ('RSSI' in c) {
device.rssi = c.RSSI;
this.emit('rssiUpdate', id, c.RSSI);
}
if ('Connected' in c) {
if (c.Connected) {
// Wait for ServicesResolved to fire 'connect' with services available.
// If ServicesResolved already true (cached device), fire now.
if (device.servicesResolved && device.connectPromise) {
device.connectPromise.resolve();
device.connectPromise = null;
}
} else {
this._onDeviceDisconnected(id, 'remote');
}
}
if ('ServicesResolved' in c) {
device.servicesResolved = !!c.ServicesResolved;
if (c.ServicesResolved && device.connectPromise) {
device.connectPromise.resolve();
device.connectPromise = null;
}
}
};
props.on('PropertiesChanged', device.propsListener);
return device;
}
_onDeviceDisconnected (id, reason) {
const device = this._devices.get(id);
if (!device) return;
if (device.proxy && device.propsListener) {
const props = device.proxy.getInterface(PROPS_IFACE);
props.off('PropertiesChanged', device.propsListener);
}
device.proxy = null;
device.propsListener = null;
device.servicesResolved = false;
if (device.connectPromise) {
device.connectPromise.reject(new Error(`disconnected: ${reason}`));
device.connectPromise = null;
}
this._removeDeviceCharListeners(id);
this.emit('disconnect', id, reason);
}
_removeDeviceCharListeners (id) {
const device = this._devices.get(id);
if (!device || !device.path) return;
const prefix = `${device.path}/`;
for (const [path, entry] of this._charPropsListeners) {
if (path.startsWith(prefix)) {
entry.props.off('PropertiesChanged', entry.listener);
this._charPropsListeners.delete(path);
}
}
}
// ---- Connect / disconnect ----
connect (peripheralUuid, _parameters) {
peripheralUuid = normalizeId(peripheralUuid);
this._connect(peripheralUuid).catch(err => {
this.emit('connect', peripheralUuid, err);
});
}
async _connect (id) {
const device = await this._ensureDeviceProxy(id);
const iface = device.proxy.getInterface(DEVICE_IFACE);
const cached = this._objects.get(device.path) || {};
const deviceProps = cached[DEVICE_IFACE] || {};
if (deviceProps.Connected && deviceProps.ServicesResolved) {
device.servicesResolved = true;
this.emit('connect', id, null);
return;
}
const waitConnected = new Promise((resolve, reject) => {
device.connectPromise = { resolve, reject };
});
try {
await iface.Connect();
} catch (err) {
device.connectPromise = null;
throw err;
}
await waitConnected;
this.emit('connect', id, null);
}
cancelConnect (peripheralUuid, _parameters) {
this.disconnect(normalizeId(peripheralUuid));
}
disconnect (peripheralUuid) {
peripheralUuid = normalizeId(peripheralUuid);
this._disconnect(peripheralUuid).catch(err => {
this.emit('warning', `disconnect failed: ${err.message}`);
});
}
async _disconnect (id) {
const device = this._devices.get(id);
if (!device) return;
if (!device.proxy) return;
const iface = device.proxy.getInterface(DEVICE_IFACE);
try {
await iface.Disconnect();
} catch (err) {
debug('Disconnect call failed: %s', err.message);
}
}
updateRssi (peripheralUuid) {
peripheralUuid = normalizeId(peripheralUuid);
const device = this._devices.get(peripheralUuid);
if (!device || !device.path) {
this.emit('rssiUpdate', peripheralUuid, 0, new Error('unknown peripheral'));
return;
}
const cached = this._objects.get(device.path) || {};
const props = cached[DEVICE_IFACE] || {};
const rssi = typeof props.RSSI === 'number' ? props.RSSI : (device.rssi || 0);
this.emit('rssiUpdate', peripheralUuid, rssi);
}
// ---- Service / characteristic / descriptor discovery ----
_findServicesForDevice (id) {
const device = this._devices.get(id);
if (!device || !device.path) return [];
const prefix = `${device.path}/`;
const services = [];
for (const [path, ifaces] of this._objects) {
if (!path.startsWith(prefix)) continue;
const svc = ifaces[GATT_SERVICE_IFACE];
if (!svc) continue;
services.push({ path, uuid: normalizeUuid(svc.UUID), primary: !!svc.Primary });
}
return services;
}
_findCharacteristicsForService (servicePath) {
const prefix = `${servicePath}/`;
const result = [];
for (const [path, ifaces] of this._objects) {
if (!path.startsWith(prefix)) continue;
const ch = ifaces[GATT_CHAR_IFACE];
if (!ch) continue;
result.push({
path,
uuid: normalizeUuid(ch.UUID),
properties: flagsToProperties(ch.Flags)
});
}
return result;
}
_findDescriptorsForCharacteristic (charPath) {
const prefix = `${charPath}/`;
const result = [];
for (const [path, ifaces] of this._objects) {
if (!path.startsWith(prefix)) continue;
const d = ifaces[GATT_DESC_IFACE];
if (!d) continue;
result.push({ path, uuid: normalizeUuid(d.UUID) });
}
return result;
}
discoverServices (peripheralUuid, uuids) {
peripheralUuid = normalizeId(peripheralUuid);
const wanted = (uuids || []).map(normalizeUuid);
const found = this._findServicesForDevice(peripheralUuid);
const filtered = wanted.length === 0 ? found : found.filter(s => wanted.includes(s.uuid));
const serviceUuids = filtered.map(s => s.uuid);
this.emit('servicesDiscover', peripheralUuid, serviceUuids);
this.emit('servicesDiscovered', peripheralUuid, serviceUuids);
}
discoverIncludedServices (peripheralUuid, serviceUuid, _serviceUuids) {
peripheralUuid = normalizeId(peripheralUuid);
// BlueZ does not expose included services directly via D-Bus.
this.emit('includedServicesDiscover', peripheralUuid, serviceUuid, []);
}
discoverCharacteristics (peripheralUuid, serviceUuid, characteristicUuids) {
peripheralUuid = normalizeId(peripheralUuid);
const services = this._findServicesForDevice(peripheralUuid);
const service = services.find(s => s.uuid === normalizeUuid(serviceUuid));
if (!service) {
this.emit('characteristicsDiscover', peripheralUuid, serviceUuid, [], new Error('service not found'));
return;
}
const wanted = (characteristicUuids || []).map(normalizeUuid);
const all = this._findCharacteristicsForService(service.path);
const filtered = wanted.length === 0 ? all : all.filter(c => wanted.includes(c.uuid));
const result = filtered.map(c => ({ uuid: c.uuid, properties: c.properties }));
this.emit('characteristicsDiscover', peripheralUuid, serviceUuid, result);
this.emit('characteristicsDiscovered', peripheralUuid, serviceUuid, result);
}
_findCharacteristicPath (peripheralUuid, serviceUuid, characteristicUuid) {
const services = this._findServicesForDevice(peripheralUuid);
const service = services.find(s => s.uuid === normalizeUuid(serviceUuid));
if (!service) return null;
const chars = this._findCharacteristicsForService(service.path);
const ch = chars.find(c => c.uuid === normalizeUuid(characteristicUuid));
return ch ? ch.path : null;
}
_findDescriptorPath (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) {
const charPath = this._findCharacteristicPath(peripheralUuid, serviceUuid, characteristicUuid);
if (!charPath) return null;
const descs = this._findDescriptorsForCharacteristic(charPath);
const d = descs.find(x => x.uuid === normalizeUuid(descriptorUuid));
return d ? d.path : null;
}
read (peripheralUuid, serviceUuid, characteristicUuid) {
peripheralUuid = normalizeId(peripheralUuid);
this._readChar(peripheralUuid, serviceUuid, characteristicUuid).catch(err => {
this.emit('read', peripheralUuid, serviceUuid, characteristicUuid, null, false, err);
});
}
async _readChar (peripheralUuid, serviceUuid, characteristicUuid) {
const path = this._findCharacteristicPath(peripheralUuid, serviceUuid, characteristicUuid);
if (!path) throw new Error('characteristic not found');
const proxy = await this._bus.getProxyObject(BLUEZ_SERVICE, path);
const iface = proxy.getInterface(GATT_CHAR_IFACE);
const value = await iface.ReadValue({});
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
this.emit('read', peripheralUuid, serviceUuid, characteristicUuid, buf, false);
}
write (peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse) {
peripheralUuid = normalizeId(peripheralUuid);
this._writeChar(peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse).catch(err => {
this.emit('write', peripheralUuid, serviceUuid, characteristicUuid, err);
});
}
async _writeChar (peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse) {
const path = this._findCharacteristicPath(peripheralUuid, serviceUuid, characteristicUuid);
if (!path) throw new Error('characteristic not found');
const proxy = await this._bus.getProxyObject(BLUEZ_SERVICE, path);
const iface = proxy.getInterface(GATT_CHAR_IFACE);
const { Variant } = this._dbus;
const options = { type: new Variant('s', withoutResponse ? 'command' : 'request') };
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
await iface.WriteValue(buf, options);
this.emit('write', peripheralUuid, serviceUuid, characteristicUuid);
}
broadcast (peripheralUuid, serviceUuid, characteristicUuid, _broadcast) {
peripheralUuid = normalizeId(peripheralUuid);
this.emit('warning', 'broadcast is not supported on the dbus backend');
this.emit('broadcast', peripheralUuid, serviceUuid, characteristicUuid, false);
}
notify (peripheralUuid, serviceUuid, characteristicUuid, notify) {
peripheralUuid = normalizeId(peripheralUuid);
this._setNotify(peripheralUuid, serviceUuid, characteristicUuid, notify).catch(err => {
this.emit('notify', peripheralUuid, serviceUuid, characteristicUuid, false, err);
});
}
async _setNotify (peripheralUuid, serviceUuid, characteristicUuid, notify) {
const path = this._findCharacteristicPath(peripheralUuid, serviceUuid, characteristicUuid);
if (!path) throw new Error('characteristic not found');
const proxy = await this._bus.getProxyObject(BLUEZ_SERVICE, path);
const iface = proxy.getInterface(GATT_CHAR_IFACE);
const props = proxy.getInterface(PROPS_IFACE);
if (notify) {
if (!this._charPropsListeners.has(path)) {
const listener = (ifaceName, changed) => {
if (ifaceName !== GATT_CHAR_IFACE) return;
const c = unwrapDict(changed);
if ('Value' in c) {
const buf = Buffer.isBuffer(c.Value) ? c.Value : Buffer.from(c.Value);
this.emit('read', peripheralUuid, serviceUuid, characteristicUuid, buf, true);
}
};
props.on('PropertiesChanged', listener);
this._charPropsListeners.set(path, { props, listener });
}
await iface.StartNotify();
this.emit('notify', peripheralUuid, serviceUuid, characteristicUuid, true);
} else {
try {
await iface.StopNotify();
} catch (err) {
debug('StopNotify failed: %s', err.message);
}
const entry = this._charPropsListeners.get(path);
if (entry) {
entry.props.off('PropertiesChanged', entry.listener);
this._charPropsListeners.delete(path);
}
this.emit('notify', peripheralUuid, serviceUuid, characteristicUuid, false);
}
}
discoverDescriptors (peripheralUuid, serviceUuid, characteristicUuid) {
peripheralUuid = normalizeId(peripheralUuid);
const charPath = this._findCharacteristicPath(peripheralUuid, serviceUuid, characteristicUuid);
if (!charPath) {
this.emit('descriptorsDiscover', peripheralUuid, serviceUuid, characteristicUuid, [], new Error('characteristic not found'));
return;
}
const descs = this._findDescriptorsForCharacteristic(charPath).map(d => d.uuid);
this.emit('descriptorsDiscover', peripheralUuid, serviceUuid, characteristicUuid, descs);
}
readValue (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) {
peripheralUuid = normalizeId(peripheralUuid);
this._readDesc(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid).catch(err => {
this.emit('valueRead', peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, null, err);
});
}
async _readDesc (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) {
const path = this._findDescriptorPath(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid);
if (!path) throw new Error('descriptor not found');
const proxy = await this._bus.getProxyObject(BLUEZ_SERVICE, path);
const iface = proxy.getInterface(GATT_DESC_IFACE);
const value = await iface.ReadValue({});
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value);
this.emit('valueRead', peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, buf);
}
writeValue (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) {
peripheralUuid = normalizeId(peripheralUuid);
this._writeDesc(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data).catch(err => {
this.emit('valueWrite', peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, err);
});
}
async _writeDesc (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) {
const path = this._findDescriptorPath(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid);
if (!path) throw new Error('descriptor not found');
const proxy = await this._bus.getProxyObject(BLUEZ_SERVICE, path);
const iface = proxy.getInterface(GATT_DESC_IFACE);
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
await iface.WriteValue(buf, {});
this.emit('valueWrite', peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid);
}
readHandle (peripheralUuid, handle) {
peripheralUuid = normalizeId(peripheralUuid);
const err = new Error('readHandle is not supported on the dbus backend (BlueZ exposes UUIDs only)');
this.emit('handleRead', peripheralUuid, handle, null, err);
}
writeHandle (peripheralUuid, handle, _data, _withoutResponse) {
peripheralUuid = normalizeId(peripheralUuid);
const err = new Error('writeHandle is not supported on the dbus backend (BlueZ exposes UUIDs only)');
this.emit('handleWrite', peripheralUuid, handle, err);
}
addressToId (address) {
return addressToId(address);
}
}
module.exports = DbusBindings;