@iotize/ionic
Version:
Iotize specific building blocks on top of @ionic/angular.
830 lines (828 loc) • 102 kB
JavaScript
import { Injectable, NgZone } from '@angular/core';
import { Platform, ToastController } from '@ionic/angular';
import { isCodeError } from '@iotize/common/error';
import { parseTapNdefMessage } from '@iotize/device-com-nfc.cordova';
import { HostProtocol, Tap, TapError, TapResponse, TapResponseStatusError, } from '@iotize/tap';
import { INITIAL_SESSION_STATE, _TAP_EXTENSION_AUTH_, } from '@iotize/tap/auth';
import { ResultCode, } from '@iotize/tap/client/api';
import { TapRequestHelper } 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 { ComProtocol, ConnectionState, } from '@iotize/tap/protocol/api';
import { _TAP_SERVICE_ALL_EXTENSIONS_, } from '@iotize/tap/service/all';
import { BehaviorSubject, Subject, of, } from 'rxjs';
import { distinctUntilChanged, filter, shareReplay, skip, startWith, switchMap, tap, } from 'rxjs/operators';
import './extensions';
import { DataManagerIonic } from './extensions/data-manager';
import { debug } from './logger';
import { isSameTag } from './nfc/utility';
import { ProtocolFactoryService } from './protocol-factory.service';
import { runInZone } from './rx-utility/run-in-zone';
import { createBleName } from './utility';
import * as i0 from "@angular/core";
import * as i1 from "@ionic/angular";
import * as i2 from "./protocol-factory.service";
const TAG = 'CurrentDeviceService';
export function LONG_RANGE_PROTOCOL_FILTER(meta) {
return meta.type !== 'nfc';
}
function getProtocolOrUndefined(client) {
try {
return client.getCurrentProtocol();
}
catch (err) {
return undefined;
}
}
export 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');
}
}
export class CurrentDeviceService {
platform;
toastCtrl;
protocolFactory;
ngZone;
/**
* Hack to prevent angular treeshaking from removing loaded extension.
*/
_loadedTapExtensions = [
_TAP_EXTENSION_DATA_,
_TAP_EXTENSION_DATA_LOG_,
_TAP_EXTENSION_KEEP_ALIVE_,
_TAP_SERVICE_ALL_EXTENSIONS_,
_TAP_EXTENSION_AUTH_,
factoryReset,
];
_tapOrUndefined = new BehaviorSubject(undefined);
_tap;
/**
* Only connection lost event
*/
_connectionLost$ = new Subject();
_sessionState$ = this._tapOrUndefined.pipe(switchMap((tapDeviceOrUndefined) => {
if (!tapDeviceOrUndefined) {
return of(INITIAL_SESSION_STATE);
}
else {
return tapDeviceOrUndefined.auth.sessionState;
}
}));
listeners = [];
keepAlivePeriod = 10 * 1000;
meta = {};
/**
* @deprecated
*/
_dataManager;
/**
* @deprecated
*/
_tapConfig$ = new BehaviorSubject(undefined);
_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
*/
tapOrUndefinedChangedWithLastValue = this._tapOrUndefined.pipe(distinctUntilChanged());
/**
* Event trigger when currrent Tap changed (set or unset)
* If tap is removed, value will be undefined
*/
tapOrUndefinedChanged = this.tapOrUndefinedChangedWithLastValue.pipe(skip(1));
/**
* Event trigger when a new Tap is selected (no event when tap is removed)
*/
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
*/
tapChangedWithLastValue = this.tapOrUndefinedChangedWithLastValue.pipe(filter((tap) => !!tap));
/**
* Event triggered when tap is removed
*/
tapRemoved = new Subject();
_protocolMeta = new BehaviorSubject(undefined);
_availableProtocols = new BehaviorSubject([]);
_isManualDisconnection = false;
_sessionStateSnapshot = INITIAL_SESSION_STATE;
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, '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, '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, '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;
debug(TAG, '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.DISCONNECTED) {
this.keepAliveEngine?.stop();
if (!this._isManualDisconnection) {
this._connectionLost$?.next(event);
}
}
else if (event.newState == ConnectionState.CONNECTED) {
this.ngZone.runOutsideAngular(() => {
if (this.keepAlivePeriod > 0) {
debug(TAG, 'Start keep alive with period', this.keepAlivePeriod);
this.keepAliveEngine?.start();
}
else {
debug(TAG, '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, 'Protocol meta', meta, 'already exists. Replacing infos');
protocols[protocolIndex] = meta;
this._availableProtocols.next(protocols);
}
else {
debug(TAG, '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, '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, 'Using long range protocol: ', protocolMeta);
await this.useProtocolFromMeta(protocolMeta);
return protocolMeta;
}
else {
debug(TAG, '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$;
}
/**
* Connection state events with replay.
* It works even when tap is changed
*/
connectionStateReplay = this.tapOrUndefinedChangedWithLastValue.pipe(switchMap((tapDevice) => {
if (!tapDevice) {
return of({
newState: ConnectionState.DISCONNECTED,
oldState: ConnectionState.DISCONNECTED,
});
}
const currentProtocol = getProtocolOrUndefined(tapDevice.client);
return tapDevice.client.onProtocolChange().pipe(startWith({
newProtocol: currentProtocol,
}), switchMap((event) => {
if (!event.newProtocol) {
return of({
newState: ConnectionState.DISCONNECTED,
oldState: ConnectionState.DISCONNECTED,
});
}
return event.newProtocol.onConnectionStateChange().pipe(runInZone(this.ngZone), startWith({
newState: event.newProtocol.getConnectionState(),
oldState: ConnectionState.DISCONNECTED,
}));
}));
}), shareReplay(1));
/**
* Any connection state change
*/
_connectionState$ = this.connectionStateReplay.pipe(skip(1));
/**
* @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, '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, '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, 'Throwing error on connection failed');
throw err;
}
}
/**
* Disconnect Tap
*/
async disconnect() {
debug(TAG, 'disconnect');
if (!this.hasProtocol()) {
return;
}
this._isManualDisconnection = true;
debug(TAG, '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, '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, '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, '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, '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, '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, '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 */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CurrentDeviceService, deps: [{ token: i1.Platform }, { token: i1.ToastController }, { token: i2.ProtocolFactoryService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
/** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CurrentDeviceService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CurrentDeviceService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: i1.Platform }, { type: i1.ToastController }, { type: i2.ProtocolFactoryService }, { type: i0.NgZone }] });
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3VycmVudC1kZXZpY2Uuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lvdGl6ZS1pb25pYy9zcmMvbGliL2N1cnJlbnQtZGV2aWNlLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFFbkQsT0FBTyxFQUFFLFFBQVEsRUFBRSxlQUFlLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUMzRCxPQUFPLEVBQWEsV0FBVyxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDOUQsT0FBTyxFQUFXLG1CQUFtQixFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDOUUsT0FBTyxFQUNMLFlBQVksRUFDWixHQUFHLEVBQ0gsUUFBUSxFQUNSLFdBQVcsRUFDWCxzQkFBc0IsR0FDdkIsTUFBTSxhQUFhLENBQUM7QUFDckIsT0FBTyxFQUNMLHFCQUFxQixFQUVyQixvQkFBb0IsR0FDckIsTUFBTSxrQkFBa0IsQ0FBQztBQUMxQixPQUFPLEVBR0wsVUFBVSxHQUVYLE1BQU0sd0JBQXdCLENBQUM7QUFDaEMsT0FBTyxFQUFhLGdCQUFnQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFFdEUsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDNUQsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDcEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQzdELE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBRXhFLE9BQU8sRUFDTCxXQUFXLEVBQ1gsZUFBZSxHQUVoQixNQUFNLDBCQUEwQixDQUFDO0FBQ2xDLE9BQU8sRUFFTCw0QkFBNEIsR0FDN0IsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQ0wsZUFBZSxFQUdmLE9B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