rxpoweredup
Version:
A Typescript RxJS-based library for controlling LEGO Powered UP hubs & peripherals.
170 lines (169 loc) • 7.54 kB
JavaScript
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;
}
}