@genee/bluez
Version:
Bluez5 D-Bus library for easy to use bluetooth access in node
398 lines (333 loc) • 13.4 kB
JavaScript
const EventEmitter = require('events').EventEmitter;
const DBus = require('@genee/dbus');
const util = require('util');
const Adapter = require('./Adapter');
const Device = require('./Device');
const Agent = require('./Agent');
const StaticKeyAgent = require('./StaticKeyAgent');
const Profile = require('./Profile');
const SerialProfile = require('./SerialProfile');
const SimplePairingAgent = require('./SimplePairingAgent');
class Bluez extends EventEmitter {
constructor(options) {
super();
this.options = Object.assign({
service: null, // connection local service
objectPath: "/org/node/bluez"
}, options);
this.bus = this.options.bus || this.getUserService().bus;//DBus.getBus('system');
if (this.options.service && typeof this.options.service !== "string")
this.userService = this.options.service;
this.getInterface = util.promisify(this.bus.getInterface.bind(this.bus));
this.tree = {};
}
async init() {
if (this.objectManager) this.objectManager.removeAllListeners();
this.objectManager = await this.getInterface('org.bluez', '/', 'org.freedesktop.DBus.ObjectManager');
this.agentManager = await this.getInterface('org.bluez', '/org/bluez', 'org.bluez.AgentManager1');
this.profileManager = await this.getInterface('org.bluez', '/org/bluez', 'org.bluez.ProfileManager1');
this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this));
this.objectManager.on('InterfacesRemoved', this.onInterfaceRemoved.bind(this));
await new Promise((resolve, reject) => {
this.objectManager.GetManagedObjects((err, objs) => {
if (err) return reject(err);
Object.keys(objs).forEach((k) => this.onInterfacesAdded(k, objs[k]))
resolve();
});
});
}
/**
* Get a Bluetooth Adapter
* @param {string} [dev] Adapter name e.g. hci0, if not supplied returns first available adapter
* @returns {Promise<Adapter>}
*/
async getAdapter(dev) {
if (dev) {
const match = dev.match(new RegExp("^/org/bluez/(\\w+)$"));
if (!match) dev = "/org/bluez/" + dev;
const res = this.getInterfaceInstance(dev, Adapter.INTERFACE_NAME, Adapter);
if (!res) throw new Error("Adapter not found");
return res;
}
// No name given, use first available Adapter
const adapters = this.getAllInterfaceProps(Adapter.INTERFACE_NAME, "/org/bluez");
const firstAdapter = Object.entries(adapters)[0];
if (!firstAdapter) throw new Error("No Adapter Available");
return this.getInterfaceInstance(firstAdapter[0], Adapter.INTERFACE_NAME, Adapter, firstAdapter[1]);
}
/**
* Find an Adapter by Props
* @param {(props)=>boolean} filterFn
* @returns {Promise<Adapter|null>}
*/
findAdapter(filterFn) {
return this.findInterfaceInstance(Adapter.INTERFACE_NAME, Adapter, filterFn);
}
/**
* Get a Device by address.
* This searches all Adapters.
* @param {string} address
* @returns {Promise<Device>}
*/
async getDevice(address) {
// normalize address
const match = address.match(new RegExp("^/org/bluez/(\\w+)/dev_(\\w+)$"));
if (match) address = match[2];
address = address.replace(/_/g, ":");
const res = await this.findInterfaceInstance(Device.INTERFACE_NAME, Device, (d) => d.Address === address);
if (!res) throw new Error("Device not found");
return res;
}
/**
* Find a Device by Props
* @example Bluez.findDevice((props) => props.Name === "Test Device")
* @param {(props)=>boolean} filterFn
* @returns {Promise<Device|null>}
*/
findDevice(filterFn) {
return this.findInterfaceInstance(Device.INTERFACE_NAME, Device, filterFn);
}
/**
* Get all available Devices.
* Note: only returns properties, not instances
*/
getAllDevicesProps() {
const devProps = this.getAllInterfaceProps(Device.INTERFACE_NAME, "/org/bluez");
return Object.values(devProps);
}
/*
This registers a profile implementation.
If an application disconnects from the bus all
its registered profiles will be removed.
HFP HS UUID: 0000111e-0000-1000-8000-00805f9b34fb
Default RFCOMM channel is 6. And this requires
authentication.
Available options:
string Name
Human readable name for the profile
string Service
The primary service class UUID
(if different from the actual
profile UUID)
string Role
For asymmetric profiles that do not
have UUIDs available to uniquely
identify each side this
parameter allows specifying the
precise local role.
Possible values: "client", "server"
uint16 Channel
RFCOMM channel number that is used
for client and server UUIDs.
If applicable it will be used in the
SDP record as well.
uint16 PSM
PSM number that is used for client
and server UUIDs.
If applicable it will be used in the
SDP record as well.
boolean RequireAuthentication
Pairing is required before connections
will be established. No devices will
be connected if not paired.
boolean RequireAuthorization
Request authorization before any
connection will be established.
boolean AutoConnect
In case of a client UUID this will
force connection of the RFCOMM or
L2CAP channels when a remote device
is connected.
string ServiceRecord
Provide a manual SDP record.
uint16 Version
Profile version (for SDP record)
uint16 Features
Profile features (for SDP record)
Possible errors: org.bluez.Error.InvalidArguments
org.bluez.Error.AlreadyExists
*/
registerProfile(profile, options) {
// assert(profile instance of Profile)
const self = this;
return new Promise((resolve, reject) => {
self.profileManager.RegisterProfile(profile._DBusObject.path, profile.uuid, options, (err) => {
if (err) return reject(err);
resolve();
});
});
}
registerSerialProfile(listener, mode, options) {
if (!mode) mode = 'client';
const obj = this.getUserServiceObject();
const profile = new SerialProfile(this, obj, listener, options);
return this.registerProfile(profile, {
Name: "Node Serial Port",
Role: mode,
PSM: 0x0003
});
}
/*
This registers an agent handler.
The object path defines the path of the agent
that will be called when user input is needed.
Every application can register its own agent and
for all actions triggered by that application its
agent is used.
It is not required by an application to register
an agent. If an application does chooses to not
register an agent, the default agent is used. This
is on most cases a good idea. Only application
like a pairing wizard should register their own
agent.
An application can only register one agent. Multiple
agents per application is not supported.
The capability parameter can have the values
"DisplayOnly", "DisplayYesNo", "KeyboardOnly",
"NoInputNoOutput" and "KeyboardDisplay" which
reflects the input and output capabilities of the
agent.
If an empty string is used it will fallback to
"KeyboardDisplay".
Possible errors: org.bluez.Error.InvalidArguments
org.bluez.Error.AlreadyExists
*/
registerAgent(agent, capabilities, requestAsDefault) {
// assert(agent instance of Agent)
const self = this;
return new Promise((resolve, reject) => {
self.agentManager.RegisterAgent(agent._DBusObject.path, capabilities || "", (err) => {
if (err) return reject(err);
if (!requestAsDefault) return resolve();
self.agentManager.RequestDefaultAgent(agent._DBusObject.path, (err) => {
if (err) return reject(err);
resolve();
});
});
});
}
/**
* Helper Method to register a Agent with a static pin
* @param {string|number} pin
* @param {boolean} [requestAsDefault]
*/
registerStaticKeyAgent(pin, requestAsDefault) {
const obj = this.getUserServiceObject();
const agent = new StaticKeyAgent(this, obj, pin);
return this.registerAgent(agent, "KeyboardOnly", requestAsDefault);
}
/**
* Helper Method to register a Agent which accepts everything
* @param {boolean} [requestAsDefault]
*/
registerSimplePairingAgent(requestAsDefault) {
const obj = this.getUserServiceObject();
const agent = new SimplePairingAgent(this, obj);
return this.registerAgent(agent, "DisplayYesNo", requestAsDefault);
}
getUserService() {
if (!this.userService) {
this.userService = DBus.registerService('system', this.options.service);
}
return this.userService;
}
getUserServiceObject() {
if (!this.userServiceObject) {
this.userServiceObject = this.getUserService().createObject(this.options.objectPath);
}
return this.userServiceObject;
}
findInterfaceInstance(interfaceName, instanceConstructor, pathPrefix, filterFn) {
if (!filterFn && typeof pathPrefix === "function") {
filterFn = pathPrefix;
pathPrefix = "/org/bluez";
}
const devProps = this.getAllInterfaceProps(interfaceName, pathPrefix);
const devProp = Object.entries(devProps).find(d => filterFn(d[1]));
if (!devProp) return Promise.resolve(null);
const [path, devIf] = devProp;
return this.getInterfaceInstance(path, interfaceName, instanceConstructor, devIf);
}
async getInterfaceInstance(objectPath, interfaceName, instanceConstructor, devIf) {
if (!devIf) {
const node = this.getNodeFromPath(objectPath);
if (!node || !node._interfaces) return null;
devIf = node._interfaces[interfaceName];
}
if (!devIf) return null;
if (!devIf._instance) {
const dbusIf = await this.getInterface('org.bluez', objectPath, interfaceName);
if (!dbusIf) throw new Error("Interface not found");
devIf._instance = new instanceConstructor(dbusIf, this);
}
return devIf._instance;
}
getAllInterfaceProps(interfaceName, pathPrefix) {
// remove tailing /
if (pathPrefix) pathPrefix = pathPrefix.replace(/\/$/, "");
function extractInterface(path, node) {
const res = {};
if (node._interfaces[interfaceName]) {
res[path] = node._interfaces[interfaceName];
}
for (const p in node) {
if (p === "_interfaces") continue;
Object.assign(res, extractInterface(path + "/" + p, node[p]));
}
return res;
}
let next = pathPrefix ? this.getNodeFromPath(pathPrefix) : this.tree;
if (!next) return {};
return extractInterface(pathPrefix || "", next);
}
getNodeFromPath(path) {
let next = this.tree;
for (const p of path.split("/")) {
if (!p) continue;
if (!next[p]) {
return null;
}
next = next[p];
}
return next;
}
async onInterfacesAdded(path, interfaces) {
//console.log("Interface Added", path, interfaces)
let next = this.tree;
for (const p of path.split("/")) {
if (!p) continue;
//console.log(p, next);
if (!next[p]) {
next[p] = {};
}
next = next[p];
}
next._interfaces = Object.assign(next._interfaces || {}, interfaces);
if ('org.bluez.Device1' in interfaces) {
const props = interfaces['org.bluez.Device1'];
this.emit('device', props.Address, props);
}
}
async onInterfaceRemoved(path, interfaces/*:string[]*/) {
//console.log("Interface Removed", path, interfaces)
let next = this.tree;
for (const p of path.split("/")) {
if (!p) continue;
//console.log(p, next);
if (!next[p]) {
next[p] = {};
}
next = next[p];
}
if (next._interfaces) {
for (const intf of interfaces) {
if (next._interfaces[intf] && next._interfaces[intf]._instance) {
// TODO: warning ???
next._interfaces[intf]._instance.emit("interface-removed");
}
delete next._interfaces[intf];
}
}
}
}
module.exports = Bluez;