rewriting the ble part

This commit is contained in:
Marcus Westin 2019-12-15 16:34:07 +01:00
parent 1a940282a3
commit 9a84afada0
3 changed files with 344 additions and 5 deletions

339
plejd/ble.js Normal file
View file

@ -0,0 +1,339 @@
const noble = require('@abandonware/noble');
const crypto = require('crypto');
const xor = require('buffer-xor');
const _ = require('lodash');
const EventEmitter = require('events');
const sleep = require('sleep');
let debug = 'console';
const getLogger = () => {
const consoleLogger = msg => console.log('plejd', msg);
if (debug === 'console') {
return consoleLogger;
}
// > /dev/null
return _.noop;
};
const logger = getLogger();
// UUIDs
const PLEJD_SERVICE = "31ba000160854726be45040c957391b5"
const DATA_UUID = "31ba000460854726be45040c957391b5"
const LAST_DATA_UUID = "31ba000560854726be45040c957391b5"
const AUTH_UUID = "31ba000960854726be45040c957391b5"
const PING_UUID = "31ba000a60854726be45040c957391b5"
const STATE_IDLE = 'idle';
const STATE_SCANNING = 'scanning';
const STATE_CONNECTING = 'connecting';
const STATE_CONNECTED = 'connected';
const STATE_AUTHENTICATED = 'authenticated';
const STATE_DISCONNECTED = 'disconnected';
const STATE_UNINITIALIZED = 'uninitialized';
const STATE_INITIALIZED = 'initialized';
class PlejdService extends EventEmitter {
constructor(cryptoKey, keepAlive = false) {
super();
this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex');
// Keeps track of the current state
this.state = STATE_IDLE;
// Keeps track of discovered devices
this.devices = {};
// Keeps track of the currently connected device
this.device = null;
// Holds a reference to all characteristics
this.characteristicState = STATE_UNINITIALIZED;
this.characteristics = {
data: null,
lastData: null,
auth: null,
ping: null
};
this._wireEvents();
}
scan() {
logger('scan()');
if (this.state === STATE_SCANNING) {
console.log('error: already scanning, please wait.');
return;
}
this.state = STATE_SCANNING;
noble.startScanning();
setTimeout(() => {
noble.stopScanning();
this.state = STATE_IDLE;
this.devices.sort((a, b) => (a.rssi > b.rssi) ? 1 : -1)
this.emit('scanComplete', this.devices);
}, 5000);
}
connect(uuid = null) {
if (!uuid) {
this.device = Object.values(this.devices)[0];
}
else {
this.device = this.devices[uuid];
if (!this.device) {
console.log('error: could not find a device with uuid: ' + uuid);
return;
}
}
this.device.connect(this.onDeviceConnected);
}
authenticate() {
logger('authenticate()');
const self = this;
if (this.state !== STATE_CONNECTED) {
console.log('error: need to be connected and not previously authenticated (new connection).');
return;
}
this.characteristics.auth.write(Buffer.from([0]), false, (err) => {
if (err) {
console.log('error: failed to authenticate: ' + err);
return;
}
self.characteristics.auth.read((err, data) => {
if (err) {
console.log('error: failed to read auth response: ' + err);
return;
}
var resp = self._createChallengeResponse(self.cryptoKey, data);
self.characteristics.auth.write(resp, false, (err) => {
if (err) {
console.log('error: failed to challenge: ' + err);
return;
}
self.state = STATE_AUTHENTICATED;
self.emit('authenticated');
});
})
});
}
onAuthenticated() {
// Start ping
logger('onAuthenticated()');
this.startPing();
}
startPing() {
logger('startPing()');
clearInterval(this.pingRef);
this.pingRef = setInterval(async () => {
if (this.state === STATE_AUTHENTICATED) {
logger('ping');
this.ping();
}
else {
console.log('error: ping failed, not connected.');
}
}, 3000);
}
onPingSuccess(nr) {
logger('pong: ' + nr);
}
onPingFailed(error) {
logger('onPingFailed(' + error + ')');
logger('stopping ping and reconnecting.');
clearInterval(this.pingRef);
this.state = STATE_DISCONNECTED;
this.connect(this.device.id);
}
ping() {
logger('ping()');
if (this.state !== STATE_AUTHENTICATED) {
console.log('error: needs to be authenticated before pinging.');
return;
}
const self = this;
var ping = crypto.randomBytes(1);
try {
this.characteristics.ping.write(ping, false, (err) => {
if (err) {
console.log('error: unable to send ping: ' + err);
self.emit('pingFailed');
return;
}
this.pingCharacteristic.read((err, data) => {
if (err) {
console.log('error: unable to read ping: ' + err);
self.emit('pingFailed');
return;
}
if (((ping[0] + 1) & 0xff) !== data[0]) {
self.emit('pingFailed');
return;
}
else {
self.emit('pingSuccess', data[0]);
}
});
});
}
catch (error) {
console.log('error: writing to plejd: ' + error);
self.emit('pingFailed', error);
}
}
onDeviceConnected(err) {
logger('onDeviceConnected()');
const self = this;
this.state = STATE_CONNECTED;
if (this.characteristicState === STATE_UNINITIALIZED) {
// We need to discover the characteristics
this.device.discoverSomeServicesAndCharacteristics([PLEJD_SERVICE], [], async (err, services, characteristics) => {
if (err) {
console.log('error: failed to discover services: ' + err);
return;
}
characteristics.forEach((ch) => {
if (DATA_UUID == ch.uuid) {
logger('found DATA characteristic.');
self.characteristics.data = ch;
}
else if (LAST_DATA_UUID == ch.uuid) {
logger('found LAST_DATA characteristic.');
self.characteristics.lastData = ch;
}
else if (AUTH_UUID == ch.uuid) {
logger('found AUTH characteristic.');
self.characteristics.auth = ch;
}
else if (PING_UUID == ch.uuid) {
logger('found PING characteristic.');
self.characteristics.ping = ch;
}
});
if (self.dataCharacteristic
&& self.lastDataCharacteristic
&& self.authCharacteristic
&& self.pingCharacteristic) {
self.characteristicState = STATE_INITIALIZED;
self.emit('deviceCharacteristicsComplete', self.device);
}
});
}
}
onDeviceCharacteristicsComplete(device) {
logger('onDeviceCharacteristicsComplete(' + device.id + ')');
this.authenticate();
}
onDeviceDiscovered(device) {
logger('onDeviceDiscovered(' + device.id + ')');
this.devices[device.id] = device;
}
onDeviceDisconnected() {
logger('onDeviceDisconnected()');
if (!this.device) {
console.log('warning: reconnect will not be performed.');
return;
}
// we just want to reconnect
this.connect(this.device.id);
}
onDeviceScanComplete() {
logger('onDeviceScanComplete()');
}
onInterfaceStateChanged(state) {
logger('onInterfaceStateChanged(' + state + ')');
if (state === 'poweredOn') {
this.scan();
}
}
wireEvents() {
noble.on('stateChanged', this.onInterfaceStateChanged);
noble.on('scanStop', this.onDeviceScanComplete);
noble.on('discover', this.onDeviceDiscovered);
noble.on('disconnect', this.onDeviceDisconnected);
this.on('deviceCharacteristicsComplete', this.onDeviceCharacteristicsComplete);
this.on('authenticated', this.onAuthenticated);
this.on('pingFailed', this.onPingFailed);
this.on('pingSuccess', this.onPingSuccess);
}
_createChallengeResponse(key, challenge) {
const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest();
const part1 = intermediate.subarray(0, 16);
const part2 = intermediate.subarray(16);
const resp = xor(part1, part2);
return resp;
}
_encryptDecrypt(key, addr, data) {
var buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]);
var cipher = crypto.createCipheriv("aes-128-ecb", key, '');
cipher.setAutoPadding(false);
var ct = cipher.update(buf).toString('hex');
ct += cipher.final().toString('hex');
ct = Buffer.from(ct, 'hex');
var output = "";
for (var i = 0, length = data.length; i < length; i++) {
output += String.fromCharCode(data[i] ^ ct[i % 16]);
}
return Buffer.from(output, 'ascii');
}
_reverseBuffer(src) {
var buffer = Buffer.allocUnsafe(src.length)
for (var i = 0, j = src.length - 1; i <= j; ++i, --j) {
buffer[i] = src[j]
buffer[j] = src[i]
}
return buffer
}
}

View file

@ -13,10 +13,10 @@
"usb": "^1.6.0"
}
},
"@abandonware/noble": {
"version": "1.9.2-5",
"resolved": "https://registry.npmjs.org/@abandonware/noble/-/noble-1.9.2-5.tgz",
"integrity": "sha512-Y1eyxDoA9kvKeAgd6mQ9c4qDbqQbqlPR56LkbtlAqptGB4HT/8KQweqqyTsj4CtdhbvCAt1G+J+2nE35WU9fBg==",
"@icanos/noble": {
"version": "1.9.2-6",
"resolved": "https://registry.npmjs.org/@icanos/noble/-/noble-1.9.2-6.tgz",
"integrity": "sha512-+NxEW7nNEueqX8MknTdno3AZeFode56tzN+dMDW0TJjW96XG822DhoHmHeQZRylTd74r/8M5c4Sb9x/m45yiPw==",
"requires": {
"@abandonware/bluetooth-hci-socket": "^0.5.3-3",
"debug": "^4.1.1",

View file

@ -1,7 +1,7 @@
{
"dependencies": {
"@abandonware/bluetooth-hci-socket": "0.5.3-3",
"@abandonware/noble": "^1.9.2-5",
"@icanos/noble": "^1.9.2-6",
"axios": "^0.19.0",
"buffer-xor": "^2.0.2",
"fs": "0.0.1-security",