UNPKG

rxpoweredup

Version:

A Typescript RxJS-based library for controlling LEGO Powered UP hubs & peripherals.

170 lines (169 loc) 7.54 kB
import { Observable, ReplaySubject, catchError, delay, from, fromEvent, of, share, switchMap, take, tap, throwError, timeout } from 'rxjs'; import { HUB_CHARACTERISTIC_UUID, HUB_SERVICE_UUID } from '../constants'; export class Hub { device; logger; config; hubConnectionErrorFactory; outboundMessengerFactory; propertiesFeatureFactory; ioFeatureFactory; characteristicsDataStreamFactory; commandsFeatureFactory; genericErrorReplyParser; messageListenerFactory; hubActionsFeatureFactory; ledFeatureFactory; gattServerDisconnectEventName = 'gattserverdisconnected'; _ports; _motors; _led; _properties; _isConnected = false; _disconnected$ = new ReplaySubject(1); _actionsFeature; outboundMessenger; constructor(device, logger, config, hubConnectionErrorFactory, outboundMessengerFactory, propertiesFeatureFactory, ioFeatureFactory, characteristicsDataStreamFactory, commandsFeatureFactory, genericErrorReplyParser, messageListenerFactory, hubActionsFeatureFactory, ledFeatureFactory) { this.device = device; this.logger = logger; this.config = config; this.hubConnectionErrorFactory = hubConnectionErrorFactory; this.outboundMessengerFactory = outboundMessengerFactory; this.propertiesFeatureFactory = propertiesFeatureFactory; this.ioFeatureFactory = ioFeatureFactory; this.characteristicsDataStreamFactory = characteristicsDataStreamFactory; this.commandsFeatureFactory = commandsFeatureFactory; this.genericErrorReplyParser = genericErrorReplyParser; this.messageListenerFactory = messageListenerFactory; this.hubActionsFeatureFactory = hubActionsFeatureFactory; this.ledFeatureFactory = ledFeatureFactory; } get willSwitchOff() { if (!this._actionsFeature) { throw new Error('Hub not connected'); } return this._actionsFeature.willSwitchOff; } get willDisconnect() { if (!this._actionsFeature?.willDisconnect) { throw new Error('Hub not connected'); } return this._actionsFeature.willDisconnect; } get ports() { if (!this._ports) { throw new Error('Hub not connected'); } return this._ports; } get motors() { if (!this._motors) { throw new Error('Hub not connected'); } return this._motors; } get rgbLight() { if (!this._led) { throw new Error('Hub not connected'); } return this._led; } get properties() { if (!this._properties) { throw new Error('Hub not connected'); } return this._properties; } get disconnected() { if (!this._disconnected$) { throw new Error('Hub not connected'); } return this._disconnected$; } connect() { return new Observable((subscriber) => { if (this._isConnected) { subscriber.error(new Error('Hub already connected')); return () => void 0; } this.logger.debug('Connecting to GATT server'); const sub = from(this.connectGattServer(this.device)) .pipe(switchMap((gatt) => from(gatt.getPrimaryService(HUB_SERVICE_UUID))), tap(() => this.logger.debug('Got primary service')), switchMap((primaryService) => from(primaryService.getCharacteristic(HUB_CHARACTERISTIC_UUID))), tap(() => { this.logger.debug('Got primary characteristic'); fromEvent(this.device, this.gattServerDisconnectEventName) .pipe(take(1), tap(() => this.logger.debug('GATT server disconnected'))) .subscribe({ complete: () => { this._ports?.dispose(); this.outboundMessenger?.dispose(); this._isConnected = false; this._disconnected$.next(); this._disconnected$.complete(); this.logger.debug('Disconnected subject completed'); }, }); }), timeout(this.config.hubConnectionTimeoutMs), switchMap((primaryCharacteristic) => from(this.createFeatures(primaryCharacteristic))), tap(() => { this._isConnected = true; this.logger.debug('Hub connection successful'); }), take(1), catchError((e) => { if (e instanceof Error) { this.logger.error(e); return of(null).pipe(delay(1000), tap(() => { this.device.gatt.disconnect(); this._disconnected$.next(); this._isConnected = false; }), switchMap(() => throwError(() => e))); } return throwError(() => e); })) .subscribe(subscriber); return () => sub.unsubscribe(); }); } disconnect() { return new Observable((subscriber) => { if (!this._actionsFeature) { subscriber.error(new Error('Hub not connected')); return () => void 0; } this.logger.debug('Disconnect invoked'); const sub = this._actionsFeature.disconnect().subscribe(subscriber); return () => sub.unsubscribe(); }); } switchOff() { return new Observable((subscriber) => { if (!this._actionsFeature) { subscriber.error(new Error('Hub not connected')); return () => void 0; } this.logger.debug('Switch off invoked'); const sub = this._actionsFeature.switchOff().subscribe(subscriber); return () => sub.unsubscribe(); }); } async createFeatures(primaryCharacteristic) { const dataStream = this.characteristicsDataStreamFactory.create(primaryCharacteristic, { incomingMessageMiddleware: this.config.incomingMessageMiddleware, }); const genericErrorsStream = this.messageListenerFactory.create(dataStream, this.genericErrorReplyParser, this._disconnected$).pipe(share()); this.outboundMessenger = this.outboundMessengerFactory.create(dataStream, genericErrorsStream, primaryCharacteristic, this._disconnected$, this.logger, this.config); this._actionsFeature = this.hubActionsFeatureFactory.create(dataStream, this.outboundMessenger, this._disconnected$); this._ports = this.ioFeatureFactory.create(dataStream, this._disconnected$, this.outboundMessenger); this._properties = this.propertiesFeatureFactory.create(dataStream, this._disconnected$, this.outboundMessenger, this.logger); this._motors = this.commandsFeatureFactory.createMotorsFeature(this.outboundMessenger, this.config); this._led = this.ledFeatureFactory.createFeature(this.outboundMessenger); await primaryCharacteristic.startNotifications(); this.logger.debug('Started primary characteristic notifications'); } async connectGattServer(device) { let gatt = null; for (let i = 0; i < this.config.maxGattConnectRetries && !gatt; i++) { gatt = await device.gatt.connect().catch(() => null); } if (!gatt) { throw this.hubConnectionErrorFactory.createGattConnectionError(); } return gatt; } }