UNPKG

idasen-controller

Version:

Package wrapping simple controls of Idasen IKEA desk.

507 lines (409 loc) 14.1 kB
'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;