idasen-controller
Version:
Package wrapping simple controls of Idasen IKEA desk.
507 lines (409 loc) • 14.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var noble = require('@abandonware/noble');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var noble__default = /*#__PURE__*/_interopDefaultLegacy(noble);
class Store {
constructor () {
this._store = {};
}
clear = () => {
this._store = {};
}
add = (key, value) => {
if (this.exists(key)) {
throw new Error('Key already exists. Use .addWithOverwrite() to overwrite value.');
}
this.addWithOverwrite(key, value);
};
addWithOverwrite = (key, value) => {
this._store[key] = value;
};
get = (key) => {
return this._store[key];
};
exists = (key) => {
return !!this._store[key];
};
existsWithValue = (key, value) => {
return !!this._store[key] && this.get(key) === value;
};
}
const store = new Store();
class UUIDWrapper {
constructor (uuid) {
this._uuid = uuid;
}
get uuid () {
return this._uuid;
}
get uuidNoDashes () {
return this.uuid.replace(/-/g, '');
}
get uuidNoDashesLowCase () {
return this.uuidNoDashes.toLowerCase();
}
}
const CODES = {
up: '4700',
down: '4600',
preflight: '0000',
stop: 'FF00'
};
const CHARACTERISTICS = {
move: new UUIDWrapper('99FA0002-338A-1024-8A49-009C0215F78A'),
moveTo: new UUIDWrapper('99FA0031-338A-1024-8A49-009C0215F78A'),
height: new UUIDWrapper('99FA0021-338A-1024-8A49-009C0215F78A')
};
class DeskHelpers {
getHeightCharacteristic = (characteristics) => {
return this.getCharacteristicByUUID(characteristics, CHARACTERISTICS.height.uuidNoDashesLowCase);
}
getMoveCharacteristic = (characteristics) => {
return this.getCharacteristicByUUID(characteristics, CHARACTERISTICS.move.uuidNoDashesLowCase);
}
getMoveToCharacteristic = (characteristics) => {
return this.getCharacteristicByUUID(characteristics, CHARACTERISTICS.moveTo.uuidNoDashesLowCase);
}
getCharacteristicByUUID = (characteristics, uuid) => {
return characteristics.find((characteristic) => {
return characteristic.uuid === uuid;
});
}
createSimplePeripheral = (peripheral) => {
return {
name: peripheral.advertisement.localName,
address: peripheral.address,
uuid: peripheral.uuid
};
}
shouldPush = (currentItems, itemToPush) => {
if (!itemToPush.advertisement.localName) return false;
if (currentItems.some((item) => item.uuid === itemToPush.uuid)) return false;
return true;
}
}
const deskHelpers = new DeskHelpers();
const STATES = {
POWERED_ON: 'poweredOn'
};
const ADAPTER_EVENTS = {
STATE_CHANGE: 'stateChange',
DISCOVER: 'discover'
};
const SCANNING_TIME_DURATION = 4000;
class BluetoothAdapter {
constructor () {
this.discoveredPeripherals = [];
this.adapterReadyPromiseResolve = null;
this.deskFoundPromiseResolve = null;
this.isAdapterReady = this.createAdapterPromise();
this.isDeskFound = this.createDeskFoundPromise();
this.setOnStateChangeHandler();
}
createAdapterPromise = () => new Promise((resolve, reject) => {
this.adapterReadyPromiseResolve = resolve;
});
createDeskFoundPromise = () => new Promise((resolve, reject) => {
this.deskFoundPromiseResolve = resolve;
});
scan = {
start: async () => noble__default['default'].startScanningAsync([], true),
stop: async () => noble__default['default'].stopScanningAsync()
};
getDeviceByAddress = async (deviceAddress) => {
noble__default['default'].removeAllListeners(ADAPTER_EVENTS.DISCOVER);
noble__default['default'].on(ADAPTER_EVENTS.DISCOVER, this.createFindDeviceHandler(deviceAddress));
this.scan.start();
const device = await this.isDeskFound;
return device;
}
getAvailableDevices = async () => {
noble__default['default'].removeAllListeners(ADAPTER_EVENTS.DISCOVER);
noble__default['default'].on(ADAPTER_EVENTS.DISCOVER, this.handleScanning);
await this.isAdapterReady;
try {
await this.scan.start();
return new Promise((resolve) => {
setTimeout(() => {
this.scan.stop();
resolve(this.discoveredPeripherals);
}, SCANNING_TIME_DURATION);
});
} catch (error) {
// TODO: change to reject?
return ({ message: 'Unable to scan.' });
}
}
handleScanning = (peripheral) => {
if (deskHelpers.shouldPush(this.discoveredPeripherals, peripheral)) {
this.discoveredPeripherals
.push(deskHelpers.createSimplePeripheral(peripheral));
} }
createFindDeviceHandler = (deskAddress) => {
return async (peripheral) => {
console.log('peripheral discovered in finding mode');
if (peripheral.address === deskAddress) {
this.scan.stop();
this.deskFoundPromiseResolve(peripheral);
}
};
}
setOnStateChangeHandler = () => {
noble__default['default'].on(ADAPTER_EVENTS.STATE_CHANGE, async (state) => {
switch (state) {
case STATES.POWERED_ON:
this.adapterReadyPromiseResolve();
}
});
}
}
const bluetoothAdapter = new BluetoothAdapter();
const storeKeys = {
PREFLIGHT_TIME_DURATION: 'PREFLIGHT_TIME_DURATION',
MOVE_TIME_DURATION: 'MOVE_TIME_DURATION',
DEFAULT_HEIGHT_TOLERANCE_THRESHOLD: 'DEFAULT_HEIGHT_TOLERANCE_THRESHOLD',
DESK_OFFSET_HEIGHT: 'DESK_OFFSET_HEIGHT',
RAW_MIN_HEIGHT: 'RAW_MIN_HEIGHT',
RAW_MAX_HEIGHT: 'RAW_MAX_HEIGHT'
};
class HeightConverter {
constructor (store) {
this.store = store;
}
getAbsoluteHeight = (relativeHeight) => {
const deskOffset = this.store.get(storeKeys.DESK_OFFSET_HEIGHT);
return relativeHeight - deskOffset;
}
getRelativeHeight = (absoluteHeight) => {
const deskOffset = this.store.get(storeKeys.DESK_OFFSET_HEIGHT);
return absoluteHeight + deskOffset;
}
getHexRepresentation = (absoluteHeight) => {
const hexString = decimalToHexString(absoluteHeight);
const reversedBitHexString = reverseBitPairs(hexString);
return reversedBitHexString;
};
getAbsoluteHeightFromBuffer = (heightInBytes) => {
return heightInBytes.readInt16LE();
}
toCentimeters = (height) => {
return height / 100;
}
toMilimeters = (height) => {
return height * 100;
}
toHexReversed = (height) => {
return heightConverter
.getHexRepresentation(heightConverter
.getAbsoluteHeight(heightConverter
.toMilimeters(height)));
}
}
const decimalToHexString = (number) => {
let hexString = number.toString(16).toUpperCase();
while (hexString.length <= 3) {
hexString = '0' + hexString;
}
return hexString;
};
const reverseBitPairs = (hexString) => {
return hexString.substring(2) + hexString.substring(0, 2);
};
const heightConverter = new HeightConverter(store);
class Desk {
constructor (peripheral) {
this.peripheral = peripheral;
this.characteristics = {
move: null,
moveTo: null,
height: null
};
this.moveToIntervalId = null;
}
connect = async () => {
await this.peripheral.connectAsync();
}
init = async () => {
const characteristics = await this.getCharacteristicsAsync(this.peripheral);
this.setCharacteristics(characteristics);
}
setCharacteristic = (name, characteristic) => {
this.characteristics[name] = characteristic;
}
setCustomPreflightDuration = (preflightTimeDuration) => {
this.preflightTimeDuration = preflightTimeDuration;
}
getCharacteristicsAsync = async (peripheral) => {
const { characteristics } = await peripheral.discoverAllServicesAndCharacteristicsAsync();
return characteristics;
}
setCharacteristics = (characteristics) => {
console.log('settings characteristics');
this.setCharacteristic('move', deskHelpers.getMoveCharacteristic(characteristics));
this.setCharacteristic('height', deskHelpers.getHeightCharacteristic(characteristics));
this.setCharacteristic('moveTo', deskHelpers.getMoveToCharacteristic(characteristics));
}
getCurrentHeightBufferAsync = () => {
return this.characteristics.height.readAsync();
}
getCurrentHeightAsync = async () => {
const heightInBytes = await this.getCurrentHeightBufferAsync();
const rawHeight = heightConverter.getAbsoluteHeightFromBuffer(heightInBytes);
const height = heightConverter
.toCentimeters(heightConverter
.getRelativeHeight(rawHeight));
return height;
}
disconnectAsync = async () => {
await this.peripheral.disconnectAsync();
console.log('\nDesk disconnected');
}
}
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
const BufferFrom = Buffer.from;
class DeskController {
constructor (desk, store) {
this.desk = desk;
this.store = store;
}
setHeightToleranceThreshold = (heightToleranceThreshold) => {
this.store.addWithOverwrite(storeKeys.DEFAULT_HEIGHT_TOLERANCE_THRESHOLD, heightToleranceThreshold);
}
moveUpAsync = async () => {
await this.desk.characteristics.move.writeAsync(new BufferFrom(CODES.up, 'hex'), false);
// TODO: add check for speed to resolve?
}
moveDownAsync = async () => {
await this.desk.characteristics.move.writeAsync(new BufferFrom(CODES.down, 'hex'), false);
// TODO: add check for speed to resolve?
}
preflightRequestAsync = async () => {
const preflightTimeDuration = this.store.get(storeKeys.PREFLIGHT_TIME_DURATION);
await this.desk.characteristics.move.writeAsync(new BufferFrom(CODES.preflight, 'hex'), false);
await sleep(this.preflightTimeDuration || preflightTimeDuration);
}
moveToAsync = async (requestedHeight) => {
const moveLoop = await this.getMoveLoop(requestedHeight);
await this.preflightRequestAsync();
return await moveLoop();
}
getMoveLoop = async (requestedHeight) => {
const shouldStopMoving = await this.getShouldStopMoving(requestedHeight);
const moveTimeDuration = this.store.get(storeKeys.MOVE_TIME_DURATION);
const requestedHeightHex = heightConverter.toHexReversed(requestedHeight);
return async () => new Promise((resolve, reject) => {
this.moveToIntervalId = setInterval(async () => {
const currentHeight = await this.desk.getCurrentHeightAsync();
if (shouldStopMoving(currentHeight, requestedHeight)) {
clearInterval(this.moveToIntervalId);
resolve();
}
await this.moveAsync(requestedHeightHex);
}, moveTimeDuration);
});
}
stopAsync = async () => {
clearInterval(this.moveToIntervalId);
await this.desk.characteristics.move.writeAsync(new BufferFrom(CODES.stop, 'hex'), false);
}
moveAsync = async (requestedHeight) => {
const heightForTransmission = new BufferFrom(requestedHeight, 'hex');
await this.desk.characteristics.moveTo.writeAsync(heightForTransmission, false);
}
getShouldStopMoving = async (requestedHeight) => {
const isMovingUp = await this.desk.getCurrentHeightAsync() > requestedHeight;
return isMovingUp
? this.shouldStopMovingUp
: this.shouldStopMovingDown;
}
shouldStopMovingUp = (current, requested) => {
if (this.isDifferenceInThreshold(current, requested)) return true;
return current < requested;
}
shouldStopMovingDown = (current, requested) => {
if (this.isDifferenceInThreshold(current, requested)) return true;
return current >= requested;
};
isDifferenceInThreshold (current, requested) {
const heightToleranceThreshold = this.store.get(storeKeys.DEFAULT_HEIGHT_TOLERANCE_THRESHOLD);
return Math.abs(current - requested) < heightToleranceThreshold;
}
}
class DeskManager {
constructor (bluetoothAdapter) {
this.desk = null;
this.deskController = null;
this.deskAddress = null;
this.discoveredPeripherals = [];
this.bluetoothAdapter = bluetoothAdapter;
this.customDisconnectHandlers = [];
this.isDeskReady = this.createDeskPromise();
}
createDeskPromise = () => new Promise((resolve, reject) => {
this.deskReadyPromiseResolve = resolve;
});
getAvailableDevices = async () => {
return await this.bluetoothAdapter.getAvailableDevices();
}
connectAsync = async (address) => {
this.deskAddress = address;
const peripheral = await this.bluetoothAdapter.getDeviceByAddress(address);
this.desk = new Desk(peripheral);
await this.desk.connect();
await this.desk.init();
this.deskController = new DeskController(this.desk, store);
this.deskReadyPromiseResolve();
this.setOnDisconnectHandler();
this.setCustomDisconnectHandlers();
const result = this.desk
? 'success'
: 'failure';
return result;
}
disconnectAsync = async () => {
if (this.desk) {
await this.desk.disconnectAsync();
}
this.deskAddress = null;
this.desk = null;
return 'success';
}
setOnDisconnectHandler = () => {
this.desk.peripheral.once('disconnect', () => {
console.log('disconnected');
this.desk = null;
this.deskController = null;
this.discoveredPeripherals = [];
this.isDeskReady = this.createDeskPromise();
this.connectAsync(this.deskAddress);
});
}
setCustomDisconnectHandlers = () => {
this.customDisconnectHandlers.forEach((disconnectHandler) => {
this.desk.peripheral.once('disconnect', () => {
disconnectHandler();
});
});
}
addCustomDisconnectHandler = (callback) => {
this.customDisconnectHandlers.push(callback);
}
}
const setDefaultValues = () => {
store.add(storeKeys.DEFAULT_HEIGHT_TOLERANCE_THRESHOLD, 0.5);
store.add(storeKeys.DESK_OFFSET_HEIGHT, 6200);
store.add(storeKeys.MOVE_TIME_DURATION, 500);
store.add(storeKeys.PREFLIGHT_TIME_DURATION, 200);
store.add(storeKeys.RAW_MAX_HEIGHT, 12700);
store.add(storeKeys.RAW_MIN_HEIGHT, 6200);
};
setDefaultValues();
const deskManager = new DeskManager(bluetoothAdapter);
const deskSettings = {
store, storeKeys
};
exports.deskManager = deskManager;
exports.deskSettings = deskSettings;