UNPKG

@iotize/ionic

Version:

Iotize specific building blocks on top of @ionic/angular.

1,271 lines (1,256 loc) 294 kB
import * as i0 from '@angular/core'; import { Injectable, Pipe, EventEmitter, Component, Output, Input, ViewChild, Directive, NgModule, InjectionToken, Inject, HostListener, ContentChild } from '@angular/core'; import * as i2 from '@ionic/angular'; import { IonicModule } from '@ionic/angular'; import { isCodeError } from '@iotize/common/error'; import { NfcError, parseTapNdefMessage } from '@iotize/device-com-nfc.cordova'; import { AppPathType, TapResponse, Tap, TapResponseStatusError, HostProtocol, TapError, PathParameter } from '@iotize/tap'; import { _TAP_EXTENSION_AUTH_, INITIAL_SESSION_STATE } from '@iotize/tap/auth'; import { ResultCode, TAP_REQUEST_FRAME_HEADER_LENGTH, TapRequestFrame } from '@iotize/tap/client/api'; import { TapRequestHelper, TapClientError, ResultCodeTranslation } from '@iotize/tap/client/impl'; import { _TAP_EXTENSION_DATA_ } from '@iotize/tap/ext/data'; import { _TAP_EXTENSION_DATA_LOG_ } from '@iotize/tap/ext/data-log'; import { factoryReset } from '@iotize/tap/ext/factory-reset'; import { _TAP_EXTENSION_KEEP_ALIVE_ } from '@iotize/tap/ext/keep-alive'; import { ConnectionState as ConnectionState$1, ComProtocol } from '@iotize/tap/protocol/api'; import { _TAP_SERVICE_ALL_EXTENSIONS_ } from '@iotize/tap/service/all'; import { Observable, BehaviorSubject, Subject, of, merge, fromEvent, NEVER, ReplaySubject, defer, pipe, throwError } from 'rxjs'; import { switchMap, distinctUntilChanged, skip, filter, startWith, shareReplay, tap, map, share, catchError, first, takeUntil } from 'rxjs/operators'; import { InterfaceService } from '@iotize/tap/service/impl/interface'; import { TapnpassService } from '@iotize/tap/service/impl/tapnpass'; import { TargetService } from '@iotize/tap/service/impl/target'; import { MonitorEngine, MonitoringController, createMonitoringTicker, TapVariable } from '@iotize/tap/data'; import { createDebugger } from '@iotize/common/debug'; import { bufferToHexString, hexStringToBuffer } from '@iotize/common/byte-converter'; import * as i2$1 from '@iotize/app-common'; import { toError } from '@iotize/app-common'; import * as i2$2 from '@angular/router'; import * as i1 from '@angular/forms'; import { Validators, ReactiveFormsModule } from '@angular/forms'; import * as i2$3 from '@angular/common'; import { CommonModule } from '@angular/common'; import { converters } from '@iotize/tap/service/core'; import { Buffer as Buffer$1 } from 'buffer'; import { NFC } from '@awesome-cordova-plugins/nfc/ngx'; import { sleep } from '@iotize/common/utility'; import { ADMIN_USER } from '@iotize/tap/configurator'; import { WifiMode } from '@iotize/tap/service/impl/wifi'; import { getScanRecordsFromBytes, getIoTizeBleCordovaPlugin, BLEScanner } from '@iotize/device-com-ble.cordova'; import { PeripheralAdapter, WebBluetoothScanner } from '@iotize/device-com-web-bluetooth.js'; const TAP_BLE_SCANNER = 'TAP_BLE_SCANNER'; const TAP_WIFI_SCANNER = 'TAP_WIFI_SCANNER'; const TAP_NETWORK_SCANNER = 'TAP_NETWORK_SCANNER'; const CONFIGURATION_MODE_VERSION = '255.255.65535'; const FACTORY_RESET_MODE_VERSION = '0.0.0'; async function adpServiceGetAdpVersion() { const response = await this.getStatus(); response.successful(); const data = await response.rawBody(); const version = data.slice(2, 5); return { major: version[0], minor: version[1], patch: version[2], }; } TapnpassService.prototype.getAdpVersion = adpServiceGetAdpVersion; // export async function interfaceServiceGetAppType(this: InterfaceService): Promise<TapVersion>{ // let response: TapResponse<any> = await this. // } // InterfaceService.prototype.getAppType = interfaceServiceGetAppType; async function interfaceServiceGetAppPathResolved() { const response = (await this.getAppPath()); if (response.isSuccessful()) { let value = response.body(); if (value.length >= 3) { const start = value.substring(0, 3); const valueWithoutPrefix = value.substring(3); switch (start) { case AppPathType.PRIMER_CLOUD_WEB_APP_URL: value = `https://user.cloud.iotize.com/users/${valueWithoutPrefix}`; break; case AppPathType.URL: value = `${valueWithoutPrefix}`; break; case AppPathType.PRIMER_CLOUD_CONFIG_URL: value = `https://user.cloud.iotize.com/users/${valueWithoutPrefix}`; break; } } response.setBody(value); } return response; } InterfaceService.prototype.getAppPathResolved = interfaceServiceGetAppPathResolved; async function interfaceServiceGetConfigPath() { const response = (await this.getAppPath()); if (response.isSuccessful()) { let value = response.body(); const start = value.substr(0, 2); switch (start) { case '$5': value = `http://user.cloud.iotize.com/users${value.substr(2)}`; break; default: if (!value.endsWith('.json') && !value.endsWith('.xml') && !value.endsWith('.cloud')) { value = ''; } } response.setBody(value); } return response; } InterfaceService.prototype.getConfigPath = interfaceServiceGetConfigPath; // export async function targetServiceGetAppType(this: TargetService): Promise<TapResponse<string>> { // let response = await this.getProtocol(); // let protocol = response.body(); // let newResponse: TapResponse<string> = TapResponse.SUCCESS(); // switch (protocol) { // case TargetProtocol.SERIAL_VIA_TAPNPASS: // newResponse.setBody("tapnpass"); // default: // newResponse.setBody("tapnlink"); // } // return newResponse; // } async function targetServiceGetAppType() { const response = await this.getSubProtocol(); const newResponse = TapResponse.SUCCESS(); if (response.isSuccessful()) { newResponse.setBody('tapnpass'); } else { newResponse.setBody('tapnlink'); } return newResponse; } TargetService.prototype.getAppType = targetServiceGetAppType; // export async function isConnected(this: TargetService): Promise<TapResponse<boolean>> { // return this._call({ // path: "/target/connect", // methodType: "get", // responseBodyDecoder: "boolean" // }) // } // TargetService.prototype.isConnected = isConnected; const debug = createDebugger('@iotize/ionic/core'); const TAG$b = 'DataManager'; class DataHolder { constructor(items = []) { this.items = items; } item(id) { const item = this.items.find((v) => v.id == id); if (!item) { throw new Error(`Item with id "${id}" does not exist`); } return item; } add(item, replace) { if (replace || this.items.find((v) => v.id == item.id) == undefined) { debug(TAG$b, 'Added item: ', item); this.items.push(item); } return this.item(item.id); } } // export function newBundleConfigToOldBundleConfig(bundlesConfig: BundleConfig[] | undefined): BundleConfigPredefined[] { // if (!bundlesConfig) { // return []; // } // return bundlesConfig.map((bundleConfig: BundleSchemaConfig) => { // const predefinedConfig: BundleConfigPredefined = { // id: bundleConfig.id, // converter: undefined, // name: bundleConfig.name || `Bundle ${bundleConfig.id}`, // variables: (bundleConfig.variables || []).map((variableConfig: MemoryVariableConfig) => { // const variableType = VariableType[variableConfig.type]; // if (variableType === undefined) { // throw new Error(`Invalid variable type: ${variableConfig.type}`); // } // let converter: BodyConverter<any> | undefined; // if (variableConfig.converter) { // converter = variableConfig.converter; // } else { // converter = getConverterFromFormat(variableType, variableConfig.length || 1); // } // const predifinedVariableConfig: TapVariable.Config = { // id: variableConfig.id, // converter: converter, // length: variableConfig.length, // name: variableConfig.name || `Variable ${variableConfig.id}`, // type: variableType, // unit: variableConfig.unit // }; // return predifinedVariableConfig; // }) // }; // return predefinedConfig; // }); // } class DataManagerIonic { constructor(tap) { this.tap = tap; // dataStream: Observable<Record<string, any>>; // tlvConverter: TlvBundleConverter<Record<string, Uint8Array>>; // dataLogger: DataLogger; this.sourceControllers = {}; // const allVariables: AbstractVariable<VariableByType[VariableKey]>[] = Object.values(data.variables); // this.tlvConverter = new TlvBundleConverter(allVariables.map((variable: AbstractVariable<any>) => { // if (!(variable instanceof VariableConfig)) { // throw new Error(`Variable ${variable.identifier} has no config`); // } // return { // id: variable.config.id, // name: variable.config.name, // converter: variable.converter // }; // })); // this.dataStream = this.setupDataStream(); // this.dataLogger = DataLogger.fromTap( // this.tap, // { // period: 2000 // TODO change // } // ); // this.tap.variables._variables = data.variables as any; // this.tap.bundles._bundles = data.bundles as any; // Change variable stream; // Object.values(this.data.variables).map((variable: any) => { // const variableName = variable.config.name; // // console.log('Replacing monitor for ', variable); // variable._monitor = ObservableMonitor.fromObservable( // this.dataStream // .pipe( // filter(data => variableName in data), // map(data => data[variableName]), // rxTap(value => debug(TAG, `Extracted key ${variableName} from monitoring data`, value)) // ) // , // MonitorEngine.State.START // ); // }); // // Change bundle stream; // Object.values(this.data.bundles).map((bundle: any) => { // bundle._monitor = ObservableMonitor.fromObservable( // this.dataStream // .pipe( // rxTap(value => debug(TAG, `New bundle value `, value)) // ) // , // MonitorEngine.State.START // ); // // TODO factorize and clean // Object.values(bundle.variables).map((variable: any) => { // const variableName = variable.config.name; // // console.log('Replacing monitor for variable in bundle', variable); // variable._monitor = ObservableMonitor.fromObservable( // this.dataStream // .pipe( // filter(data => variableName in data), // map(data => data[variableName]), // rxTap(value => debug(TAG, `Extracted key ${variableName} from monitoring data`, value)) // ) // , // MonitorEngine.State.START // ); // }); // }); // debug(TAG, 'New data manager instance: ', this); } // get variables(): Record<VariableKey, AbstractVariable<VariableByType[VariableKey]>> { // return this.data.variables; // } // get bundles(): Record<BundleKey, Bundle<BundleByType[BundleKey]>> { // return this.data.bundles; // } get sourceController() { const currentSourceId = this.switchableStreamController.getCurrentSourceId(); return this.sourceControllers[currentSourceId]; } get isMonitoringRunning() { for (const value of Object.values(this.sourceControllers)) { if (value.state == MonitorEngine.State.START) { return true; } } return false; } // public static fromSchemaConfig(tap: Tap, schema: TapConfiguratorConfig): DataManagerIonic { // const bundles = schema.config.data ? schema.config.data.bundles || [] : []; // return DataManagerIonic.fromBundleConfig(tap, bundles); // } // public static fromBundleConfig(tap: Tap, bundles: BundleConfig[]): DataManagerIonic { // return new DataManagerIonic( // tap, // DataManagerIonic.createDataFromSchemaConfig(tap, bundles) // ); // } // public static createDataFromSchemaConfig<VariableByType extends Record<string, any>, VariableKey extends string, BundleByType extends Record<string, any>, BundleKey extends string>(tap: Tap, bundlesConfig: BundleSchemaConfig[]) { // const predifinedConfig: BundleConfigPredefined[] = newBundleConfigToOldBundleConfig(bundlesConfig); // return DataManagerIonic.createDataFromConfig(tap, predifinedConfig); // } // public static createDataFromConfig<VariableByType extends Record<string, any>, VariableKey extends string, BundleByType extends Record<string, any>, BundleKey extends string>(tap: Tap, bundlesConfig: BundleConfigPredefined[]) { // // let bundleConfigCopy = [...bundlesConfig]; // const bundles: Record<string, any> = {}; // bundlesConfig.forEach((bundleConfig: BundleConfigPredefined) => { // const bundle: Bundle = Bundle.createFromConfig(bundleConfig, tap.service.bundle, tap.service.variable); // bundles[bundleConfig.name] = bundle; // }); // const variables: Record<string, any> = {}; // bundlesConfig.forEach((bundle) => { // bundle.variables.forEach((variableConfig: TapVariable.Config) => { // debug(TAG, 'Create from variable config: ', variableConfig); // variables[variableConfig.name || variableConfig.id.toString()] = TapVariable.createFromConfig(variableConfig, tap.service.variable); // }); // }); // return { // bundles: bundles as Record<BundleKey, Bundle<BundleByType[BundleKey]>>, // variables: variables as Record<VariableKey, AbstractVariable<VariableByType[VariableKey]>> // }; // } listVariables() { return this.tap.data.listVariables(); } listBundles() { return this.tap.data.listBundles(); } // public variable<T extends VariableKey>(key: T): AbstractVariable<VariableByType[T]> { // if (!(key in this.variables)) { // throw new Error(`Variable with identifier "${key}" is not configured`); // } // return this.variables[key] as AbstractVariable<VariableByType[T]>; // } // public bundle<T extends BundleKey>(key: T): Bundle<BundleByType[T]> { // if (!(key in this.bundles)) { // throw new Error(`Bundle with identifier "${key}" is not configured`); // } // return this.bundles[key] as any; // } stopAll() { debug(TAG$b, 'Stop monitoring'); // for (const ctrlKey in this.sourceControllers) { // this.sourceControllers[ctrlKey].state = MonitorEngine.State.PAUSE; // } this.tap.data.monitoring.stop(); } startAll(period) { // debug(TAG, `[Controller=${this.sourceController.id}] Start monitoring with period ${period || this.sourceController.period.value}`, this.sourceController); // if (period) { // this.setMonitoringPeriod(period); // } // this.sourceController.state = MonitorEngine.State.START; this.tap.data.monitoring.start(); } setMonitoringPeriod(period) { debug(TAG$b, 'Changing monitoring period to ', period); this.sourceController.period.next(period); } destroy() { this.stopAll(); } refresh() { if (this.sourceController.refresh) { this.sourceController.refresh(); } } changeDataSource(dataSource) { switch (dataSource) { case 'datalog': this.useDatalogSource(); break; default: this.useLiveSource(); } } useLiveSource() { this.switchableStreamController.useSource('live'); } useDatalogSource() { this.switchableStreamController.useSource('datalog'); } } function isSameTag(t1, t2) { return (bufferToHexString(t1.id) === bufferToHexString(t2.id)); } function isNfcTapRequiredError(err) { const cause = getCauseError(err); return (isCodeError(NfcError.ErrorCode.TagLostError, cause) || isCodeError(NfcError.ErrorCode.NotConnectedError, cause) || isCodeError(NfcError.ErrorCode.Unknown, cause) || isCodeError(NfcError.ErrorCode.InternalError, cause)); } function getCauseError(err) { let depth = 0; while (err.cause && depth < 5) { err = err.cause; depth++; } return err; } /** * Do not use this classe. * Used for angular DI * Create a class that implemement this one */ class ProtocolFactoryService { constructor() { } create(meta) { throw new Error(`Unsupported protocol type "${meta.type}"`); } isValid(meta) { return false; } } function runInZone(zone) { return (source) => { return new Observable((observer) => { const onNext = (value) => zone.run(() => observer.next(value)); const onError = (e) => zone.run(() => observer.error(e)); const onComplete = () => zone.run(() => observer.complete()); return source.subscribe(onNext, onError, onComplete); }); }; } class LibError { static missingFromField(fieldKey) { return new Error(`InternalError: form must have a field with name "${fieldKey}"`); } static componentArgumentRequired(component, input) { return new Error(`InternalError: ${component} requires input "${input}"`); } static abstractVariableRequired() { return new Error('InternalError: AbstractVariable cannot be undefined'); } } function createBleName(appName, serialNumber) { return appName + '_' + serialNumber.substr(serialNumber.length - 5); } function getFormFieldOrError(form, key) { const field = form.get(key); if (!field) { throw LibError.missingFromField(key); } return field; } function getAbstractVariableOrError(variable) { if (!variable) { throw LibError.abstractVariableRequired(); } else { return variable; } } const TAG$a = 'CurrentDeviceService'; function LONG_RANGE_PROTOCOL_FILTER(meta) { return meta.type !== 'nfc'; } function getProtocolOrUndefined(client) { try { return client.getCurrentProtocol(); } catch (err) { return undefined; } } class TapServiceError extends Error { static illegalArgument(msg) { throw new Error(`Illegal argument: ${msg}`); } static illegalStateNoTap() { return new TapServiceError('Illegal state: Tap is not set yet'); } } class CurrentDeviceService { get sessionStateSnapshot() { return this._sessionStateSnapshot; } get sessionState() { return this._sessionState$; } get protocolMeta$() { return this._protocolMeta.asObservable(); } get availableProtocols$() { return this._availableProtocols.asObservable(); } get protocolMeta() { return this._protocolMeta.value; } set protocolMeta(meta) { if (meta) { this.addProtocolMeta(meta); } debug(TAG$a, 'setting protocol meta', meta); this._protocolMeta.next(meta); } get connectionLost() { return this._connectionLost$.asObservable(); } get availableProtocols() { return this._availableProtocols.value; } get tap() { if (!this._tap) { throw new Error('Connect to a device first'); // throw TapServiceError.illegalStateNoTap(); } return this._tap; } get tapOrUndefined() { return this._tap; } get hasTap() { return this._tap !== undefined; } set tapConfig(schema) { debug(TAG$a, 'Set Tap config', schema); this.dataManager = new DataManagerIonic(this.tap); this._tapConfig$.next(schema); if (schema) { if (schema.config?.data) { // this.tap.data.clear(); this.tap.data.configureWithDataConfig(schema.config.data); /* this.tap.data.values.subscribe(v => { }) this.tap.data.monitoring.asSubject() .subscribe(state => { }) */ } else { this.tap.data.clear(); } // // this.dataLogger.converter = new DataLogPacketConverter( // newBundleConfigToOldBundleConfig(schema.config.data.bundles) // ); // debug(TAG, 'Updating datalog converter'); // this.dataLogger.converter = DataLogPacketConverter.createFromManager(this.tap.bundles, this.tap.variables); // if (schema.config.data) { // this.dataLogger.converter = new DataLogPacketConverter( // newBundleConfigToOldBundleConfig(schema.config.data.bundles || []) // ); // } } } get tapConfig() { return this._tapConfig$.value; } get tapConfig$() { return this._tapConfig$; } isSameTag(tag) { if (this.protocolMeta && this.protocolMeta.type === 'nfc') { // Check is it's the same tag const currentTag = this.protocolMeta.info.tag; if (currentTag.id && currentTag.id?.length == 0) { return false; } return isSameTag(currentTag, tag); } else { // It's not nfc currently const nfcProtocolMeta = this.availableProtocols.find((p) => p.type === 'nfc'); if (nfcProtocolMeta) { return isSameTag(nfcProtocolMeta.info.tag, tag); } } return false; } /** * Use another communicaiton protocol * May be rejected * @param meta: ProtocolMeta * @param disonnectCurrentProtocol if set to true and if tap is already connected with * a communication protocol, it will disconnect from it first * @param connectToNew: boolean */ async useProtocol(meta, disonnectCurrentProtocol = true, connectToNew = true) { debug(TAG$a, 'use protocol', meta); const protocol = await this.protocolFactory.create(meta); if (!this._tap) { this.tap = Tap.fromProtocol(protocol); } else { // let oldConnectionState = this._tap.protocol.getConnectionState(); if (disonnectCurrentProtocol) { try { await this.tap.protocol.disconnect().toPromise(); } catch (err) { console.warn('Cannot disconnect current protocol properly: ', err); } } this._tap.useComProtocol(protocol); } this.protocolMeta = meta; if (connectToNew) { await this.connect(); } } async executeFactoryReset() { await this.tap.factoryReset(); this.tap.auth.clearCache(); this.tap.encryption.stop(); } set tap(t) { this.setTap(t, { emit: true }); } constructor(platform, toastCtrl, protocolFactory, ngZone) { this.platform = platform; this.toastCtrl = toastCtrl; this.protocolFactory = protocolFactory; this.ngZone = ngZone; /** * Hack to prevent angular treeshaking from removing loaded extension. */ this._loadedTapExtensions = [ _TAP_EXTENSION_DATA_, _TAP_EXTENSION_DATA_LOG_, _TAP_EXTENSION_KEEP_ALIVE_, _TAP_SERVICE_ALL_EXTENSIONS_, _TAP_EXTENSION_AUTH_, factoryReset, ]; this._tapOrUndefined = new BehaviorSubject(undefined); /** * Only connection lost event */ this._connectionLost$ = new Subject(); this._sessionState$ = this._tapOrUndefined.pipe(switchMap((tapDeviceOrUndefined) => { if (!tapDeviceOrUndefined) { return of(INITIAL_SESSION_STATE); } else { return tapDeviceOrUndefined.auth.sessionState; } })); this.listeners = []; this.keepAlivePeriod = 10 * 1000; this.meta = {}; /** * @deprecated */ this._tapConfig$ = new BehaviorSubject(undefined); this._maxFrameSizeCache = {}; /** * Event trigger when currrent Tap changed (set or unset) * If tap is removed, value will be undefined * Immediatly triggered the current value when subscribing */ this.tapOrUndefinedChangedWithLastValue = this._tapOrUndefined.pipe(distinctUntilChanged()); /** * Event trigger when currrent Tap changed (set or unset) * If tap is removed, value will be undefined */ this.tapOrUndefinedChanged = this.tapOrUndefinedChangedWithLastValue.pipe(skip(1)); /** * Event trigger when a new Tap is selected (no event when tap is removed) */ this.tapChanged = this.tapOrUndefinedChanged.pipe(filter((tap) => !!tap)); /** * Event trigger when a new Tap is selected (no event when tap is removed) * Immediatly triggered the current value when subscribing */ this.tapChangedWithLastValue = this.tapOrUndefinedChangedWithLastValue.pipe(filter((tap) => !!tap)); /** * Event triggered when tap is removed */ this.tapRemoved = new Subject(); this._protocolMeta = new BehaviorSubject(undefined); this._availableProtocols = new BehaviorSubject([]); this._isManualDisconnection = false; this._sessionStateSnapshot = INITIAL_SESSION_STATE; /** * Connection state events with replay. * It works even when tap is changed */ this.connectionStateReplay = this.tapOrUndefinedChangedWithLastValue.pipe(switchMap((tapDevice) => { if (!tapDevice) { return of({ newState: ConnectionState$1.DISCONNECTED, oldState: ConnectionState$1.DISCONNECTED, }); } const currentProtocol = getProtocolOrUndefined(tapDevice.client); return tapDevice.client.onProtocolChange().pipe(startWith({ newProtocol: currentProtocol, }), switchMap((event) => { if (!event.newProtocol) { return of({ newState: ConnectionState$1.DISCONNECTED, oldState: ConnectionState$1.DISCONNECTED, }); } return event.newProtocol.onConnectionStateChange().pipe(runInZone(this.ngZone), startWith({ newState: event.newProtocol.getConnectionState(), oldState: ConnectionState$1.DISCONNECTED, })); })); }), shareReplay(1)); /** * Any connection state change */ this._connectionState$ = this.connectionStateReplay.pipe(skip(1)); debug(TAG$a, 'NEW INSTANCE', this._loadedTapExtensions); this.tapChangedWithLastValue.subscribe((newTap) => { newTap.client.addInterceptor((context, next) => { return next.handle(context).pipe(tap((tapResponseFrame) => { const response = new TapResponse(tapResponseFrame, context.request); if (response.status === ResultCode.UNAUTHORIZED) { if (TapRequestHelper.pathToString(context.request.header.path) !== this.tap.service.interface.resources.login.path) { this.listeners.forEach((listener) => { if (listener.onTapRequestUnauthorized) { listener.onTapRequestUnauthorized({ request: context, response: response, }); } }); } } if (!response.isSuccessful()) { this.listeners.forEach((listener) => { if (listener.onTapRequestError) { listener.onTapRequestError({ request: context.request, error: new TapResponseStatusError(response), response: response, }); } }); } }, (error) => { this.listeners.forEach((listener) => { console.warn(`Request ${context.request} errored: ${error}`); if (listener.onTapRequestError) { listener.onTapRequestError({ request: context.request, error: error, }); } }); })); }); }); this.sessionState.subscribe((newSessionState) => { this._sessionStateSnapshot = newSessionState; }); this.connectionStateReplay.subscribe(async (event) => { if (event.newState === ConnectionState$1.DISCONNECTED) { this.keepAliveEngine?.stop(); if (!this._isManualDisconnection) { this._connectionLost$?.next(event); } } else if (event.newState == ConnectionState$1.CONNECTED) { this.ngZone.runOutsideAngular(() => { if (this.keepAlivePeriod > 0) { debug(TAG$a, 'Start keep alive with period', this.keepAlivePeriod); this.keepAliveEngine?.start(); } else { debug(TAG$a, 'NO keep alive'); } }); } this.listeners.forEach((listener) => listener.onTapConnectionStateChange(event)); }); } addProtocolMeta(meta) { if (meta) { const protocols = this.availableProtocols; const protocolIndex = protocols.findIndex((existing) => { return existing.type === meta.type; }); if (protocolIndex >= 0) { debug(TAG$a, 'Protocol meta', meta, 'already exists. Replacing infos'); protocols[protocolIndex] = meta; this._availableProtocols.next(protocols); } else { debug(TAG$a, 'Adding protocol meta', meta); protocols.push(meta); this._availableProtocols.next(protocols); } } } /** * Parse NDefTag and try to create a ProtocolMeta thanks to record * @returns undefined if there is no other protocol in the tag */ async registerProtocolsFromTag(tag) { let meta; if (!tag.ndefMessage) { return undefined; } const info = parseTapNdefMessage(tag.ndefMessage); if (info.macAddress && info.macAddress !== '00:00:00:00:00:00') { meta = { type: 'ble', info: { mac: info.macAddress, name: info.name, }, }; } else if (info.ssid) { const hostname = (await this.tap.service.wifi.getHostname()).body(); meta = { type: 'wifi', info: { ssid: info.ssid, // name: info.name, url: `tcp://${hostname}:2000`, }, }; } if (meta) { this.addProtocolMeta(meta); } return meta; } async getCurrentHostProtocolMaxFrameSizeCacheFirst() { const protocol = this.protocolMeta?.type; if (!protocol || !this._maxFrameSizeCache[protocol]) { const hostProtocol = (await this.tap.service.interface.getCurrentHostProtocolMaxFrameSize()).body(); if (hostProtocol.request > 0xff) { hostProtocol.request -= 2; // due to 2 more bytes for apdu.header.lc } if (protocol) { this._maxFrameSizeCache[protocol] = hostProtocol; } else { return hostProtocol; } } return this._maxFrameSizeCache[protocol]; } /** * Will register available communication protocols on current tap * by asking LWM2M resources. * @returns the list of new ProtocolMeta found */ async registerProtocolsFromTap() { const nProtocolMeta = []; const [appNameResponse, serialNumberResponse, authorizedHostProtocolsResponse, bleAddressResponse, ipResponse,] = await this.tap.service.interface.executeMultipleCalls([ this.tap.service.interface.getAppNameCall(), this.tap.service.device.getSerialNumberCall(), this.tap.service.interface.getAuthorizedHostProtocolCall(), this.tap.service.ble.getAddressCall(), this.tap.service.wifi.getIpCall(), ]); const authorizedHostProtocols = authorizedHostProtocolsResponse.body(); if (this.protocolMeta?.type !== 'ble' && authorizedHostProtocols.includes(HostProtocol.BLE)) { if (this.platform.is('ios')) { const bleName = createBleName(appNameResponse.body(), serialNumberResponse.body()); nProtocolMeta.push({ type: 'ble', info: { name: bleName, }, }); } else { nProtocolMeta.push({ type: 'ble', info: { mac: bleAddressResponse.body(), }, }); } } if (this.protocolMeta?.type !== 'socket' && this.platform.is('mobile') && authorizedHostProtocols.includes(HostProtocol.WIFI)) { const ip = ipResponse.body(); if (ip && ip !== '0.0.0.0') { nProtocolMeta.push({ type: 'socket', info: { url: 'tcp://' + ip + ':2000', }, }); } } nProtocolMeta.forEach((meta) => this.addProtocolMeta(meta)); return nProtocolMeta; } async useProtocolFromMeta(newProtocolMeta, disonnectCurrentProtocol = true, connectToNew = true) { if (!this.protocolMeta || newProtocolMeta.type !== this.protocolMeta.type) { return await this.useProtocol(newProtocolMeta, disonnectCurrentProtocol, connectToNew); } } /** * Switch from NFC communication protocol to a long range communication * @warning this is only available when we are in NFC * * @return the new protocol meta used or undefined if it does not have a long range protocol to use */ async useLongRangeProtocol() { debug(TAG$a, 'useLongRangeProtocol'); const currentProtocol = this.protocolMeta; if (!this._tap) { throw TapServiceError.illegalStateNoTap(); } if (currentProtocol) { if (currentProtocol.type === 'nfc') { const protocolMeta = this.availableProtocols.find(LONG_RANGE_PROTOCOL_FILTER); if (protocolMeta) { debug(TAG$a, 'Using long range protocol: ', protocolMeta); await this.useProtocolFromMeta(protocolMeta); return protocolMeta; } else { debug(TAG$a, 'NFC tag does not have long range protocol information...'); } } } return undefined; } /** * Connection state events. * It works even when tap is changed */ get connectionState() { return this._connectionState$; } /** * @returns true if user is connected as given username or one of the given usernames */ async isLoggedInAsUserOrProfileName(userOrProfileNames) { if (typeof userOrProfileNames === 'string') { userOrProfileNames = [userOrProfileNames]; } if (typeof userOrProfileNames === 'number') { console.warn('invalid parameter for CurrentDeviceService.isLoggedInAsUserOrProfileName() username should be a string'); userOrProfileNames = [userOrProfileNames.toString()]; } if (!this._tap || !this.tap.auth.sessionStateSnapshot) { return false; } const sessionState = this.tap.auth.sessionStateSnapshot || (await this.tap.auth.refreshSessionState()); return (userOrProfileNames.includes(sessionState.profileName) || userOrProfileNames.includes(sessionState.name)); } async login(username, password, refreshSessionState = true) { return await this.tap.auth.login({ username, password }, { noRefreshSessionState: !refreshSessionState, }); } async logout(throwErr = false) { if (!this._tap) { return false; } try { await this._tap.auth.logout(); return true; } catch (err) { if (throwErr) { throw err; } else { this.onError.bind(this); return false; } } } setTap(newTap, options = { emit: true }) { if (!newTap) { throw new Error('Illegal state: cannot set undefined tap'); } this._tap = newTap; debug(TAG$a, 'Setting tap...'); this.meta = {}; this._tap = newTap; if (this.keepAlivePeriod === 0) { newTap.keepAlive.stop(); } newTap.keepAlive.period = this.keepAlivePeriod; if (options.emit) { this.notifyNewTap(); } } notifyNewTap() { this._tapOrUndefined.next(this._tap); } setTapFromEvent(event, options = { emit: true }) { if (!event.protocolMeta) { throw new Error('Missing protocol information'); } this.protocolMeta = event.protocolMeta; // this.tap = event.tap; this.setTap(event.tap, options); debug(TAG$a, 'setTapFromEvent = >', event); } /** * Connect to the Tap and refresh session state * Throw error if it fails */ async connect( /** * @deprecated */ throwErr = true) { try { await this.tap.connect(); await this.refreshSessionState(); return true; } catch (err) { debug(TAG$a, 'Throwing error on connection failed'); throw err; } } /** * Disconnect Tap */ async disconnect() { debug(TAG$a, 'disconnect'); if (!this.hasProtocol()) { return; } this._isManualDisconnection = true; debug(TAG$a, 'Setting _isManualDisconnection flag to true'); return (this._tap ? this._tap.disconnect() : Promise.reject(TapServiceError.illegalStateNoTap())).then((res) => { // This is a temporary hack to differentiate manual/unexpected disconnection // The timeout is used to make sure that direct events are trigger before we set the manual // disconnection flag back to false // TODO remove later setTimeout(() => { debug(TAG$a, 'Setting _isManualDisconnection flag to false'); this._isManualDisconnection = false; }, 50); return res; }); } /** * Remove currently used Tap * If no Tap was set, do nothing */ async remove(disconnect = true) { if (this._tap) { const oldTap = this._tap; debug(TAG$a, 'Removing tap'); this.protocolMeta = undefined; this._availableProtocols.next([]); if (disconnect) { await this.disconnect().catch(this.onError.bind(this)); } console.log('Stopping old keep alive', oldTap.keepAlive); oldTap.keepAlive.stop(); this._tap = undefined; this._dataManager = undefined; this._maxFrameSizeCache = {}; // this._tapConfig = undefined; this.notifyTapRemoved(oldTap); } else { debug(TAG$a, 'No tap to remove'); } } notifyTapRemoved(t) { this.tapRemoved.next(t); this.notifyNewTap(); } /** * @deprecated */ async configureClientIfRequired() { if (!this.meta.isClientConfigured) { console.log('Configuring client...'); await this.configureClient().catch((err) => { this.onError(err); }); } } /** * Rebuild tap configuration according to data on the Tap * * @deprecated */ async loadConfigFromDevice() { debug(TAG$a, 'dynamically load Tap config'); const syncEvent = await this.tap.data.synchronizeTapConfig().toPromise(); this.meta.isClientConfigured = true; // TODO Fix to get real config if (syncEvent?.step === 'done') { const bundles = syncEvent.bundles; this.tapConfig = { meta: { version: '1.0.0', partial: true, }, config: { version: 1, data: { bundles: [], }, }, }; } } set dataManager(v) { debug(TAG$a, 'Setting data manager', v); if (this._dataManager) { this._dataManager.destroy(); } this._dataManager = v; } get dataManager() { if (!this._dataManager) { return undefined; } return this._dataManager; } /** * Configure client by reading tap device configuration * * @deprecated will be moved to a separate service * * @throws */ async configureClient(refresh = false) { debug(TAG$a, 'configureClient'); if (refresh) { this.tapConfig = undefined; } // TODO only once // if (this.modelConfig) { // console.info('load from iotz model configuration file') // let tapConfigConfigurator = new TapConfigConfigurator(this.modelConfig); // await this.tap.configure(tapConfigConfigurator); // this.meta.isClientConfigured = true; // console.log('Variables: ', this.tap.variables); // } if (this.tapConfig === undefined) { await this.loadConfigFromDevice(); // this.tap.bundles.clear(); // this.tap.variables.clear(); // await this.tap.configure(this.readDeviceDataConfigurator).catch(this.onError.bind(this)); } } getCurrentProtocol() { return getProtocolOrUndefined(this.tap.client); } get keepAliveEngine() { return this._tap?.keepAlive; } hasProtocol() { return this.getCurrentProtocol() !== undefined; } registerEventListerner(listener) { this.listeners.push(listener); } unregisterEventListener(listener) { const index = this.listeners.indexOf(listener); if (index >= 0) { this.listeners.splice(index, 1); } } /** * @deprecated refractor: remove on error from this service to use a global handler * * @param err */ async onError(err) { // TODO remove const toast = await this.toastCtrl.create({ message: err.message || 'Unknown error', color: 'danger', position: 'bottom', duration: 3000, buttons: [ { text: 'Close', role: 'cancel', handler: () => { console.log('Close clicked'); }, }, ], }); await toast.present(); } async reboot() { let rebootResponse; try { rebootResponse = await this.tap.service.device.reboot(); } catch (err) { if (!isCodeError(TapError.Code.ExecuteRequestError, err)) { throw err; } else { const cause = err.cause; if (cause?.code !== ComProtocol.ErrorCode.TimeoutError) { console.warn('Reboot error', err); throw err; } // ignore timeout error as tap may reboot before sending response } } if (rebootResponse) { rebootResponse.successful(); } await this.clearAuth(); this.tap.disconnect().catch((err) => { }); } async clearAuth() { this.tap.auth.clearCache(); this.tap.encryption.stop(); await this.tap.auth.logout().catch((err) => { }); } async refreshSessionState() { try { return await this.tap.auth.refreshSessionState(); } catch (err) { if (isCodeError(TapError.Code.ScramNotStartedYet, err) || isCodeError(TapError.Code.InvalidScramKey, err)) { console.warn(`SCRAM session ended due to error`, err.message); this.tap.encryption.stop(); return await this.tap.auth.refreshSessionState(); } else { throw err; } } } } /** @nocollapse */ CurrentDeviceService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CurrentDeviceService, deps: [{ token: i2.Platform }, { token: i2.ToastController }, { token: ProtocolFactoryService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ CurrentDeviceService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CurrentDeviceService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CurrentDeviceService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: i2.Platform }, { type: i2.ToastController }, { type: ProtocolFactoryService }, { type: i0.NgZone }]; } }); /** * Route resolver to make sure that a Tap is set at this point * */ class ConnectedTapResolver { constructor(tapService) { this.tapService = tapService; } async resolve(route, state) { try { await this.tapService.connect(); } catch (err) { return this.onError(err); } } onError(error) {