- 版本:1.1.0
- GitHub: https://github.com/GeoTecINIT/awarns-framework
- NPM: https://npmjs.net.cn/package/%40awarns%2Fcore
- 下载
- 昨天:6
- 上周:33
- 上个月:98
@awarns/core
这是其他插件正常工作的唯一必需插件。
此插件作为 NativeScript 任务调度器(NTD)的包装器,以多种方式扩展其功能,简化了上下文感知应用程序的开发。
- 一个常见的模型来表示随时间变化而变化的实体,即 Record 类。这个类旨在被扩展并用于封装由内置框架任务和开发人员使用框架创建的开发者定义的任务产生的信息。
- 用于开发自己的数据提供者的工具,无论是获取内部还是外部数据。
- 预定义的任务以使用您的提供者。这些为所有可以使用的内置或自定义组件提供了一个统一的数据获取过程。
- 通过单个接口导出 NTD 提供的所有内容。
- 用于简化日志记录、测试、创建唯一标识符和序列化数据的实用工具。
本质上,此插件的主要目标是提供由 NTD 定义的作业模型访问权限,并扩展它以包含用于开发数据提供者和数据提供作业的原始数据。它还提供了一个基础模型(Record),该模型可以由内置或自定义框架任务产生或消费的任何实体扩展。在这里,通过扩展 Record 模型,耗时任务,如持久性,变得极大简化。
安装核心包只需要一条命令行指令
ns plugin add @awarns/core
用法
第一次将框架集成到您的应用程序中时,您需要安装和配置核心包。
此外,核心包可以在其他情况下从您的应用程序(或插件)中可选使用
- 当您想要创建一个自己的类(扩展 Record 模型)用于其持久性。
- 当您想要开发自己的数据提供者。
- 当您想要使用内置任务将提供者纳入工作流程时。
- 在更复杂的使用场景中,当您想要使用内置实用工具时。
基本用法
初始化
为了 AwarNS 框架能够正常工作,必须在应用程序启动时进行初始化。无论应用程序 UI 是否将要启动,都必须执行此代码。执行此操作的位置是应用程序 src
文件夹中的 app.ts
文件(对于 Angular 应用程序为 main.ts
)。
框架初始化涉及多个方面: (1)确定哪些内置和/或自定义任务将被使用, (2)定义这些任务如何由其他任务的结果或隔离的应用程序事件调用, (3)注册需要在应用程序启动时初始化的插件,以及 (4)配置框架的行为方面。这可以在以下代码片段中更详细地了解,该代码片段是从演示应用程序源代码中改编的。
// app.ts / main.ts
// TypeScript App:
import { Application } from '@nativescript/core';
// or Angular App:
import { runNativeScriptAngularApp, platformNativeScript } from '@nativescript/angular';
import { AppModule } from './app/app.module';
// AwarNS Framework imports
// (always between esential imports and app initialization)
import { awarns } from '@awarns/core';
import { demoTasks } from '../tasks'; // An array, containing the lists of tasks that the application will use
import { demoTaskGraph } from '../graph'; // The background workflow definition (task graph instance)
import { registerHumanActivityPlugin } from '@awarns/human-activity';
import { registerNotificationsPlugin } from '@awarns/notifications';
import { registerTracingPlugin } from '@awarns/tracing';
import { registerPersistencePlugin } from '@awarns/persistence';
awarns
.init(
demoTasks, // (1)
demoTaskGraph, // (2)
[ // (3)
// See bellow for more information regarding the items that this array can contain
// See each plugin docs to learn more about their registration-time options
registerHumanActivityPlugin(),
registerNotificationsPlugin('Intervention alerts'),
registerPersistencePlugin(),
registerTracingPlugin(),
],
{ // (4)
// See bellow for a description of the rest of the options
enableLogging: true,
}
)
.then(() => console.log('AwarNS framework successfully loaded'))
.catch((err) => {
console.error(`Could not load AwarNS framework: ${err.stack ? err.stack : err}`);
});
// TypeScript App:
Application.run({ moduleName: 'app-root' });
// Angular App:
runNativeScriptAngularApp({
appModuleBootstrap: () => platformNativeScript().bootstrapModule(AppModule),
});
在 (3) 中,除了内置的插件注册函数外,还可以注册自定义加载器以在框架初始化阶段运行代码。您可以通过创建一个必须返回与 PluginLoader API 兼容的另一个函数的函数来实现这一点。此页面上的一个示例实现(见 实例化基于推的数据提供者任务)和 人类活动、通知、持久性 和 跟踪 插件的源代码中都可以找到。
重要:我们建议您在这里仅注册短期函数,以确保在开始执行任务之前,框架的所有功能都已准备就绪。如果您需要在这里启动一个长时间的过程,您也可以这样做,但请确保主函数不会等待其执行完成(例如,使用
then/catch
而不是await
)。如果不遵循此建议,可能会导致意外且难以调试的行为。
在 (4) 中,除了指示是否启用日志记录外,还可以传递一个自定义记录器实现以获得对记录内容的更多控制。同时,还能够将日志痕迹本地存储或远程发送。更多详细信息请参阅:关于日志记录和其他工具的简要说明。
管理任务就绪和从 UI 发射事件
在应用程序 UI 中,您可以通过与框架交互来检查某些任务是否缺少某些权限或系统功能以启用。这可以通过使用在前一个示例中看到的 awarns
单例对象来实现,该对象与 NTD 的 taskDispatcher
对象共享 API
名称 | 返回类型 | 描述 |
---|---|---|
isReady() |
Promise<boolean> |
允许检查(并等待)框架初始化状态。它还会遍历您的应用程序任务,通过调用它们的 checkIfCanRunMethod() 来检查它们是否已准备好执行。您应该在发出任何外部事件之前调用此方法。这个承诺是内部存储的,可以多次调用此方法。 |
tasksNotReady (属性) |
Promise<Array<Task>> |
如果 isReady() 返回 false,则需要调用的方法。在这里,您可以检查未通过就绪检查的任务。在调用 prepare() 之前自定义 UI 很有用。例如,向您用户解释为什么您需要他们的同意 |
prepare() |
Promise<void> |
如果 isReady() 返回 false,则需要调用的方法。如果您的应用程序有一个或多个报告尚未就绪的任务,它将调用它们的 prepare() 方法(例如,请求缺少的权限或启用禁用的功能)。警告!此方法仅应在 UI 可见时调用。遵循此指南以促进创建一致的任务生态系统。 |
emitEvent(name: string, data?: EventData) |
void |
这是一个触发并忘记的方法。当您想将外部事件传播到插件时,请调用此方法。依赖任务将在后台环境中执行。用户可以安全地导航到另一个应用程序,我们将启动一个独立的后台执行上下文以确保其完成其生命周期(我们保证最多 3 分钟的执行时间)。您可以选择提供一个额外的键值数据字典,该字典将传递给处理事件的任务 |
扩展 Record 类
Record类是AwarNS框架的核心。在您的实体中扩展它意味着它们将使用框架的数据共享通用语言。这将极大地简化某些操作,例如持久性和数据导出,仅举几例。
此类非常适合表示随时间变化的事物。Record类每个扩展(子类)都必须持有其类型,这是一个字符串。此字符串唯一标识每个记录的实体类型,这在以后是必需的,例如,为了单独持久化和查询每种类型的实体。记录必须保留一个时间戳,以指示它们何时被生成。可选的是,它们可以声明一个变化,可以是:开始、结束或无变化(无变化)。
了解如何扩展Record类的最佳方式是查看框架中的一些现有示例。
地理位置
地理位置记录
import { Record } from '@awarns/core/entities';
import { GeolocationLike as GL, Geolocation as NativeGeolocation } from 'nativescript-context-apis/geolocation';
export type GeolocationLike = GL;
export const GeolocationType = 'geolocation';
export class Geolocation extends Record {
constructor(
public latitude: number,
public longitude: number,
public altitude: number,
public horizontalAccuracy: number,
public verticalAccuracy: number,
public speed: number,
public direction: number,
capturedAt: Date
) {
super(GeolocationType, capturedAt);
}
distance(to: Geolocation | GeolocationLike) {
return new NativeGeolocation(this).distance(to);
}
}
地理围栏
import { Change, Record } from '@awarns/core/entities';
import { GeofencingProximity } from './proximity';
export const AoIProximityChangeType = 'aoi-proximity-change';
export class AoIProximityChange extends Record {
constructor(
public aoi: AreaOfInterest,
public proximity: GeofencingProximity,
change: Change,
timestamp = new Date()
) {
super(AoIProximityChangeType, timestamp, change);
}
}
export interface AreaOfInterest {
id: string;
name: string;
latitude: number;
longitude: number;
radius: number;
category?: string;
level?: number;
}
人类活动
import { Record, Change } from '@awarns/core/entities';
import { HumanActivity } from 'nativescript-context-apis/activity-recognition';
export const HumanActivityChangeType = 'human-activity';
export class HumanActivityChange extends Record {
constructor(public activity: HumanActivity, change: Change, detectedAt: Date, public confidence?: number) {
super(HumanActivityChangeType, detectedAt, change);
}
}
export { HumanActivity } from 'nativescript-context-apis/activity-recognition';
通知
您甚至可以创建记录层次结构,例如
import { NotificationEventBaseRecord } from './notification-event-base-record';
import { TapAction } from '../notification';
export const NotificationTapType = 'notification-tap';
export class NotificationTapRecord extends NotificationEventBaseRecord {
constructor(notificationId: number, tapAction: TapAction, timestamp?: Date) {
super(NotificationTapType, notificationId, tapAction, timestamp);
}
}
import { NotificationEventBaseRecord } from './notification-event-base-record';
import { TapAction } from '../notification';
export const NotificationDiscardType = 'notification-discard';
export class NotificationDiscardRecord extends NotificationEventBaseRecord {
constructor(notificationId: number, tapAction: TapAction, timestamp?: Date) {
super(NotificationDiscardType, notificationId, tapAction, timestamp);
}
}
以及两者的公共基类NotificationEventBaseRecord
import { Change, Record } from '@awarns/core/entities';
import { TapAction } from '../notification';
export abstract class NotificationEventBaseRecord extends Record {
protected constructor(
public name: string,
public notificationId: number,
public tapAction: TapAction,
timestamp: Date = new Date()
) {
super(name, timestamp, Change.NONE);
}
}
该包内还有更多示例,例如QuestionnaireAnswers、UserFeedback、UserConfirmation或UserReadContent记录。
类似地,Wi-Fi(WifiScan)、BLE(BleScan)和电池(BatteryLevel)包中也有额外的示例。还有框架的README中(见详细使用和扩展部分)。
开发您自己的数据提供者
上下文感知框架最重要的方面是能够感知环境。实现这一目标的第一步是建模将要感知的内容。我们之前通过扩展Record类已经朝这个方向迈进了一步。现在,我们需要定义如何感知/获取这些数据。
在这里,重要的是要做出区分。我们理解有两种方法可以获得数据:主动和被动。这意味着,我们可以手动拉取数据,或者我们可以在数据可用时订阅以获取数据推送。
拉取数据提供者
获取数据的常见情况是请求它,有时在短暂延迟后获得它。例如,手机的地理位置、电池级别、附近蓝牙设备的列表或Wi-Fi接入点的列表。列表不仅限于手机能提供的内容。例如,我们在执行网络请求时拉取数据(例如,获取当前的天气)。
要开发这样的数据提供者,我们需要能够编写执行以下操作的机制:(1)知道是否所有条件都已满足以获取数据(这意味着,所有权限都已授予,特定的系统功能已被启用等),(2)如果所有条件尚未满足,则需要做什么来满足它们(例如,请求权限、启用系统服务等),以及(3)确定如何获取下一次数据更新。最后一件事是必需的,因为基于拉取的数据提供者类似于迭代器。内部,框架将询问它们下一个要获取的值,即当您的应用的后台执行工作流程指定时。
话虽如此,学习如何实现基于拉取的数据提供者的最佳方式是查看一些已在框架中实现的示例。在实现新的数据提供者时,我们建议从以下之一开始作为模板。在这里,它们按从简单到复杂的顺序列出:
电池
import { BatteryLevel, BatteryLevelType } from './battery-level';
import { Application, isAndroid } from '@nativescript/core';
import { PullProvider, ProviderInterruption } from '@awarns/core/providers';
export class BatteryProvider implements PullProvider {
get provides() {
return BatteryLevelType;
}
constructor(private sdkVersion?: number) {
if (isAndroid && !this.sdkVersion) {
this.sdkVersion = android.os.Build.VERSION.SDK_INT;
}
}
next(): [Promise<BatteryLevel>, ProviderInterruption] {
const value = this.getBatteryPercentage();
const batteryLevel = BatteryLevel.fromPercentage(value);
return [Promise.resolve(batteryLevel), () => null];
}
checkIfIsReady(): Promise<void> {
return Promise.resolve();
}
prepare(): Promise<void> {
return Promise.resolve();
}
private getBatteryPercentage(): number {
if (!isAndroid) {
return -1;
}
if (this.sdkVersion >= 21) {
const batteryManager: android.os.BatteryManager = Application.android.context.getSystemService(
android.content.Context.BATTERY_SERVICE
);
return batteryManager.getIntProperty(android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY);
}
const intentFilter = new android.content.IntentFilter(android.content.Intent.ACTION_BATTERY_CHANGED);
const batteryStatus = Application.android.context.registerReceiver(null, intentFilter);
const level = batteryStatus ? batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1) : -1;
const scale = batteryStatus ? batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, -1) : -1;
const batteryPercentage = level / scale;
return Math.trunc(batteryPercentage * 100);
}
}
Wi-Fi
import { ProviderInterrupter, ProviderInterruption, PullProvider } from '@awarns/core/providers';
import { WifiScan, WifiScanType } from './scan';
import {
FingerprintGrouping,
getWifiScanProvider as getNativeProvider,
WifiFingerprint,
WifiScanProvider as NativeProvider,
} from 'nativescript-context-apis/wifi';
import { firstValueFrom, map, of, Subject, takeUntil, timeout } from 'rxjs';
export class WifiScanProvider implements PullProvider {
get provides(): string {
return WifiScanType;
}
constructor(
private ensureIsNew: boolean,
private timeout: number,
private nativeProvider: () => NativeProvider = getNativeProvider
) {}
async checkIfIsReady(): Promise<void> {
const isReady = await this.nativeProvider().isReady();
if (!isReady) {
throw wifiScanProviderNotReadyErr;
}
}
async prepare(): Promise<void> {
return this.nativeProvider().prepare();
}
next(): [Promise<WifiScan>, ProviderInterruption] {
const interrupter = new ProviderInterrupter();
const scanResult = this.obtainWifiScan(interrupter);
return [scanResult, () => interrupter.interrupt()];
}
private obtainWifiScan(interrupter: ProviderInterrupter): Promise<WifiScan> {
const interrupted$ = new Subject<void>();
interrupter.interruption = () => {
interrupted$.next();
interrupted$.complete();
};
return firstValueFrom(
this.nativeProvider()
.wifiFingerprintStream({
ensureAlwaysNew: this.ensureIsNew,
grouping: FingerprintGrouping.NONE,
continueOnFailure: false,
})
.pipe(
takeUntil(interrupted$),
timeout({ each: this.timeout, with: () => of(null) }),
map((fingerprint) => scanFromFingerprint(fingerprint))
)
);
}
}
function scanFromFingerprint(fingerprint: WifiFingerprint): WifiScan {
const { seen, isNew, timestamp } = fingerprint;
return new WifiScan(seen, isNew, timestamp);
}
export const wifiScanProviderNotReadyErr = new Error(
"Wifi scan provider is not ready. Perhaps permissions haven't been granted, location services have been disabled or wifi is turn off"
);
蓝牙低功耗(BLE)
import { ProviderInterrupter, ProviderInterruption, PullProvider } from '@awarns/core/providers';
import { BleScan, BleScanType } from './scan';
import {
getBleScanProvider as getNativeProvider,
BleScanProvider as NativeProvider,
BleScanMode,
BleScanResult,
} from 'nativescript-context-apis/ble';
import { firstValueFrom, map, Subject, takeUntil, timer, toArray } from 'rxjs';
export class BleScanProvider implements PullProvider {
get provides(): string {
return BleScanType;
}
constructor(
private scanTime: number,
private scanMode: BleScanMode,
private iBeaconUuids: Array<string>,
private nativeProvider: () => NativeProvider = getNativeProvider
) {}
async checkIfIsReady(): Promise<void> {
const isReady = await this.nativeProvider().isReady();
if (!isReady) {
throw bleScanProviderNotReadyErr;
}
}
async prepare(): Promise<void> {
return this.nativeProvider().prepare();
}
next(): [Promise<BleScan>, ProviderInterruption] {
const interrupter = new ProviderInterrupter();
const scanResult = this.obtainBleScan(interrupter);
return [scanResult, () => interrupter.interrupt()];
}
private obtainBleScan(interrupter: ProviderInterrupter): Promise<BleScan> {
const interrupted$ = new Subject<void>();
interrupter.interruption = () => {
interrupted$.next();
interrupted$.complete();
};
return firstValueFrom(
this.nativeProvider()
.bleScanStream({
reportInterval: 100 /* Lower report intervals don't seem to report anything in background*/,
scanMode: this.scanMode,
iBeaconUuids: this.iBeaconUuids,
})
.pipe(
takeUntil(timer(this.scanTime)),
toArray(),
map((results) => scanFromResults(results))
)
);
}
}
function scanFromResults(results: Array<BleScanResult>): BleScan {
if (results.length === 0) {
throw new Error('No BLE devices were found nearby!');
}
return new BleScan(
results.reduce((prev, curr) => [...prev, ...curr.seen], []),
results[results.length - 1].timestamp
);
}
const bleScanProviderNotReadyErr = new Error(
"BLE scan provider is not ready. Perhaps permissions haven't been granted, location services have been disabled or Bluetooth is turn off"
);
地理位置
import { PullProvider, ProviderInterrupter, ProviderInterruption } from '@awarns/core/providers';
import { Geolocation, GeolocationType } from './geolocation';
import {
GeolocationProvider as NativeProvider,
Geolocation as NativeGeolocation,
getGeolocationProvider as getNativeProvider,
} from 'nativescript-context-apis/geolocation';
import { firstValueFrom, from, Observable, of, Subject, throwError, timeout } from 'rxjs';
import { map, mergeMap, take, takeUntil, toArray } from 'rxjs/operators';
export class GeolocationProvider implements PullProvider {
get provides(): string {
return GeolocationType;
}
constructor(
private bestOf: number,
private timeout: number,
private nativeProvider: () => NativeProvider = getNativeProvider
) {}
async checkIfIsReady(): Promise<void> {
const isReady = await this.nativeProvider().isReady();
if (!isReady) {
throw geolocationProviderNotReadyErr;
}
}
prepare(): Promise<void> {
return this.nativeProvider().prepare(false, true);
}
next(): [Promise<Geolocation>, ProviderInterruption] {
const interrupter = new ProviderInterrupter();
const bestLocation = this.obtainBestLocationAmongNext(this.bestOf, interrupter);
return [bestLocation, () => interrupter.interrupt()];
}
private obtainBestLocationAmongNext(amount: number, interrupter: ProviderInterrupter): Promise<Geolocation> {
const interrupted = new Subject<void>();
interrupter.interruption = () => {
interrupted.next();
interrupted.complete();
};
return firstValueFrom(
this.nativeProvider()
.locationStream({
highAccuracy: true,
stdInterval: 1000,
minInterval: 100,
maxAge: 60000,
saveBattery: false,
})
.pipe(
takeUntil(interrupted),
take(amount),
timeout({ each: this.timeout, with: () => of(null) }),
toArray(),
map(pickBest),
mergeMap((location) => this.ensureItGetsAtLeastOne(location)),
map(toGeolocation)
)
);
}
private ensureItGetsAtLeastOne(location: NativeGeolocation): Observable<NativeGeolocation> {
if (!location) {
return from(
this.nativeProvider().acquireLocation({
highAccuracy: true,
allowBackground: true,
})
).pipe(
timeout({
each: this.timeout,
with: () => throwError(() => new Error('Could not acquire location')),
})
);
}
return of(location);
}
}
export const geolocationProviderNotReadyErr = new Error(
"Geolocation provider is not ready. Perhaps permissions haven't been granted or location services have been disabled"
);
function pickBest(locations: Array<NativeGeolocation>): NativeGeolocation {
const now = Date.now();
return locations.reduce(
(previous, current) =>
current && (!previous || calculateScore(current, now) > calculateScore(previous, now)) ? current : previous,
null
);
}
function calculateScore(location: NativeGeolocation, now: number): number {
const { horizontalAccuracy, timestamp } = location;
const timeDiff = (now - timestamp.getTime()) / 1000;
const limitedAccuracy = Math.min(horizontalAccuracy, 65);
const limitedTimeDiff = Math.min(Math.max(timeDiff, 0), 60);
const accuracyScore = 1 - limitedAccuracy / 65;
const timeDiffScore = 1 - limitedTimeDiff / 60;
return ((accuracyScore + timeDiffScore) / 2) * 10;
}
function toGeolocation(nativeGeolocation: NativeGeolocation): Geolocation {
return new Geolocation(
nativeGeolocation.latitude,
nativeGeolocation.longitude,
nativeGeolocation.altitude,
nativeGeolocation.horizontalAccuracy,
nativeGeolocation.verticalAccuracy,
nativeGeolocation.speed,
nativeGeolocation.direction,
nativeGeolocation.timestamp
);
}
推送数据提供者
有时我们想获取数据,但我们不知道数据何时到来。在这些情况下,我们将希望指示第三方通知我们的应用程序有关数据更新的情况。例如,在人类活动识别的情况下,更新将只在设备开始移动后才会到来。
基于推送的数据提供者实现与基于拉取的数据提供者有一些共同之处。两者都需要知道它们是否能够获取数据,如果不能,则需要知道如何克服这种情况。关键的区别在于,我们不需要请求获取下一个值(并等待它),在这里我们需要两种机制来表示:1)我们感兴趣获取数据更新,2)我们不再对这些更新感兴趣。就像订阅/取消订阅机制,但它在应用程序关闭和手机重启后仍然存在。
一个完整的基于推送的数据提供者实现示例可以在人类活动包(HumanActivityProvider)中看到。
import { PushProvider } from '@awarns/core/providers';
import { ActivityRecognizer, getActivityRecognizer, Resolution } from 'nativescript-context-apis/activity-recognition';
import { HumanActivityChangeType } from './human-activity-change';
import { getLogger } from '@awarns/core/utils/logger';
import { getHumanActivityChangeReceiver } from './receiver';
const possibleResolutions: Array<Resolution> = [Resolution.LOW, Resolution.MEDIUM];
export class HumanActivityProvider implements PushProvider {
get provides() {
return HumanActivityChangeType;
}
static setup() {
possibleResolutions.forEach((resolution) => {
getActivityRecognizer(resolution).listenActivityChanges((activityChange) => {
getLogger('HumanActivityProvider').debug(`Got an activity change!: ${JSON.stringify(activityChange)}`);
getHumanActivityChangeReceiver().onReceive(activityChange);
});
});
}
constructor(
private resolution: Resolution,
private detectionInterval: number = 0,
private providerLoader: (resolution: Resolution) => ActivityRecognizer = getActivityRecognizer
) {}
async checkIfIsReady(): Promise<void> {
if (!this.activityRecognizer().isReady()) {
throw new HumanActivityRecognizerNotReadyErr(this.resolution);
}
}
async prepare(): Promise<void> {
await this.activityRecognizer().prepare();
}
async startProviding(): Promise<void> {
await this.activityRecognizer().startRecognizing({
detectionInterval: this.detectionInterval,
});
}
async stopProviding(): Promise<void> {
await this.activityRecognizer().stopRecognizing();
}
private activityRecognizer(): ActivityRecognizer {
return this.providerLoader(this.resolution);
}
}
export class HumanActivityRecognizerNotReadyErr extends Error {
constructor(resolution: Resolution) {
super(
`${resolution} resolution human activity recognizer. Perhaps the required permissions hadn't been granted. Be sure to call prepare() first`
);
}
}
export { Resolution } from 'nativescript-context-apis/activity-recognition';
使用内置任务与您的数据提供者一起使用
一旦我们开发了我们的数据提供者,将它们集成到框架中,以在我们的后台工作流程中使用它们,是非常直接的。
为此,我们创建了一系列任务,这些任务理解数据提供者的API,并以统一的方式从中获取数据。
实例化基于拉取的数据提供者任务
框架提供两种从基于拉取的数据提供者获取数据的方法:单一数据提供者和批量数据提供者。两者都按其名称所示执行。前者从数据提供者中读取一个值,并立即将其发射,而后者可以在发射之前积累多个值。
这些任务始终以相同的方式使用,尽管它们的行为可以进行配置。以下是如何使用GeolocationProvider的示例:
import { Task, SinglePullProviderTask, BatchPullProviderTask } from '@awarns/core/tasks';
import { GeolocationProvider } from './provider';
const DEFAULT_SINGLE_BEST_OF = 3;
const DEFAULT_SINGLE_TIMEOUT = 10000;
const DEFAULT_BATCH_BEST_OF = 1;
const DEFAULT_BATCH_TIMEOUT = 15000;
export function acquirePhoneGeolocationTask(config: GeolocationTaskConfig = {}): Task {
return new SinglePullProviderTask(
new GeolocationProvider(config.bestOf ?? DEFAULT_SINGLE_BEST_OF, config.timeout ?? DEFAULT_SINGLE_TIMEOUT),
'Phone',
{ foreground: true }
);
}
export function acquireMultiplePhoneGeolocationTask(config: GeolocationTaskConfig = {}): Task {
return new BatchPullProviderTask(
new GeolocationProvider(config.bestOf ?? DEFAULT_BATCH_BEST_OF, config.timeout ?? DEFAULT_BATCH_TIMEOUT),
'Phone',
{ foreground: true }
);
}
export interface GeolocationTaskConfig {
bestOf?: number;
timeout?: number;
}
这两个任务都经过精心设计,以创建可预测的输出。SinglePullProviderTask的实例将命名为:acquire{prefix?}{record-type}
,其中前缀是任务的构造函数的第二个可选参数,record-type是通过询问提供者它提供什么来获得的。另一方面,BatchPullProviderTask的实例将命名为:acquireMultiple{prefix?}{record-type}
,其中占位符以相同的方式填充。两个事件在两种情况下都是相同的:{record-type}Acquired
。唯一的区别是,单一提供者任务输出一条记录,而批量提供者任务输出它们的数组。
注意:当实现基于数据提供者结果的任务时,请考虑事件可能包含单个记录或记录数组,具体取决于它们是否来自单一或批量数据提供者任务。
这些任务允许进行一些配置。在其实例化时间,可以指定它们是否需要在前台运行,如上所示。当收集敏感数据时,这是必需的。前台通知可以通过以下特定 NTD 指令进行配置。此外,在定义工作流程时,批处理提供者任务可以被配置为限制新记录收集的最大频率,如下这里所示,通过使用maxInterval
配置参数。
注意:单个提供者任务和批处理提供者任务之间有一个很大的区别,那就是后者将不会在消耗完所有可运行任务的时间之前完成。这意味着,如果批处理任务已被安排每分钟运行一次,它将尝试在那一分钟内收集尽可能多的样本,然后报告。请记住,不要在批处理数据收集之后安排长期任务,否则它们可能根本不会运行。此任务旨在进行彻底的数据收集,且后续处理很少。
实例化基于推送的数据提供者任务
基于推送的数据提供者任务更容易实例化,但集成起来略困难。与基于拉的任务不同,设置基于推送的任务需要三个不同的步骤。
首先要做的是实例化提供者启动和停止任务。我们使用 HumanActivityProviders 提供示例。
import { Task, StartPushProviderTask, StopPushProviderTask } from '@awarns/core/tasks';
import { HumanActivityProvider, Resolution } from './provider';
export function startDetectingCoarseHumanActivityChangesTask(): Task {
return new StartPushProviderTask(new HumanActivityProvider(Resolution.LOW), 'Coarse');
}
export function stopDetectingCoarseHumanActivityChangesTask(): Task {
return new StopPushProviderTask(new HumanActivityProvider(Resolution.LOW), 'Coarse');
}
export function startDetectingIntermediateHumanActivityChangesTask(): Task {
return new StartPushProviderTask(new HumanActivityProvider(Resolution.MEDIUM), 'Intermediate');
}
export function stopDetectingIntermediateHumanActivityChangesTask(): Task {
return new StopPushProviderTask(new HumanActivityProvider(Resolution.MEDIUM), 'Intermediate');
}
与 SingleProvider 和 BatchProvider 任务类似,任务的命名已经标准化。对于启动任务,名称始终遵循以下模式:startDetecting{prefix?}{record-type}Changes
,而对于停止任务,始终是这样的:stopDetecting{prefix?}{record-type}Changes
。
与基于拉的任务的关键区别在于,我们还需要注册一个监听器来接收提供者的更新并将它们作为框架事件发出。以下是人类活动识别的示例。
import { awarns } from '@awarns/core';
import { EventData } from '@awarns/core/events';
import { ActivityChange, HumanActivity, Transition } from 'nativescript-context-apis/activity-recognition';
import { HumanActivityChange } from './human-activity-change';
import { Change } from '@awarns/core/entities';
const DEFAULT_EVENT = 'userActivityChanged';
export class HumanActivityChangeReceiver {
constructor(private emitEvent: (eventName: string, eventData?: EventData) => void) {}
onReceive(activityChange: ActivityChange) {
const { type, timestamp, confidence } = activityChange;
const change = activityChange.transition === Transition.STARTED ? Change.START : Change.END;
const record = new HumanActivityChange(type, change, timestamp, confidence);
this.emitEvent(DEFAULT_EVENT, record);
this.emitEvent(generateEventNameFromActivityChange(record), record);
}
}
function generateEventNameFromActivityChange(activityChange: HumanActivityChange) {
const transition = activityChange.change === Change.START ? 'Started' : 'Finished';
return `user${transition}${actionFromActivityType(activityChange.activity)}`;
}
function actionFromActivityType(type: HumanActivity) {
switch (type) {
case HumanActivity.STILL:
return 'BeingStill';
case HumanActivity.TILTING:
return 'StandingUp';
case HumanActivity.WALKING:
return 'Walking';
case HumanActivity.RUNNING:
return 'Running';
case HumanActivity.ON_BICYCLE:
return 'RidingABicycle';
case HumanActivity.IN_VEHICLE:
return 'BeingInAVehicle';
}
}
let _receiver: HumanActivityChangeReceiver;
export function getHumanActivityChangeReceiver(): HumanActivityChangeReceiver {
if (!_receiver) {
_receiver = new HumanActivityChangeReceiver(awarns.emitEvent);
}
return _receiver;
}
最后要做的就是将此监听器在应用程序启动时注册。最佳位置是将监听器注册封装在一个函数中,并在插件注册时调用它,如基本用法部分所示。
这是人类活动识别插件中此函数的实现方式。
import { PluginLoader } from '@awarns/core';
import { Task } from '@awarns/core/tasks';
import { HumanActivityProvider } from './internal/provider';
export function registerHumanActivityPlugin(): PluginLoader {
return async (_tasksInUse: Array<Task>) => {
HumanActivityProvider.setup();
// ...
};
}
关于日志记录和其他工具的简要说明
除了所有上述功能外,核心包还附带了一组小型工具,包括典型的可重用功能块。这些工具被分为4个不同的领域:日志记录、测试、唯一标识符和数据序列化。
日志记录
AwarNS 框架附带一个内置的控制台日志记录器。然而,在框架初始化期间,可以注入自定义日志记录器(见基本用法 - 初始化)。
甚至可以在框架的上下文中使用自定义日志记录器提交崩溃,将其发送到远程服务器。以下代码片段显示了使用将错误提交到 Firebase Crashlytics 的日志记录器的一个示例。
import {
Logger,
AbstractLogger,
} from "@awarns/core/utils/logger";
import { FirebaseManager, firebaseManager } from "../firebase";
import { DevLogger } from "./dev";
import { isAndroid } from "@nativescript/core";
export class ProdLogger extends AbstractLogger {
constructor(
tag: string,
private firebase: FirebaseManager = firebaseManager,
private auxLogger: Logger = new DevLogger(tag)
) {
super(tag);
}
protected logDebug(message: string): void {
return; // Do not print or send debug messages in production
}
protected async logInfo(message: string): Promise<void> {
const crashlytics = await this.firebase.crashlytics();
if (crashlytics) {
crashlytics.log(message);
} else {
this.auxLogger.info(message);
}
}
protected async logWarning(message: string): Promise<void> {
const crashlytics = await this.firebase.crashlytics();
if (crashlytics) {
crashlytics.log(message);
} else {
this.auxLogger.warn(message);
}
}
protected async logError(message: string): Promise<void> {
const crashlytics = await this.firebase.crashlytics();
if (!crashlytics) {
this.auxLogger.error(message);
return;
}
crashlytics.sendCrashLog(new java.lang.Exception(message));
}
}
注意:要实现自定义日志记录器,不需要扩展
AbstractLogger
类。如果您无法这样做,或者您想要有关如何打印消息有更多的自由度,您始终可以简单地实现Logger
接口。
类似于上面的日志记录器类实例声明以下公共 API(与 Logger 接口相同)
名称 | 返回类型 | 描述 |
---|---|---|
debug(message: any) |
void |
允许显示仅在开发期间有用的消息。通常您会想要有两个不同的日志记录器,一个用于开发,另一个用于生产。生产日志记录器可以简单地忽略对此方法的调用,就像上面发生的那样。 |
info(message: any) |
void |
允许显示常规信息消息,这可能在调试日志跟踪时有用。每个任务的log() 方法都会使用此方法。 |
warning(message: any) |
void |
允许记录非关键错误。 |
error(message: any) |
void |
允许记录致命的应用程序失败。 |
根据本节的所有信息,如果您想使用此可选框架功能,我们建议您实现一个函数,以创建一个或多个具有作用域标签的日志记录器(根据当前环境类型)。然后,使用 customLogger(tag: string)
属性将此新函数注入到框架配置选项对象中。
测试
在 @awarns/core/testing/events
文件夹中,您可以找到一些函数,这些函数可以帮助您独立于框架的其他部分测试自己的任务。
名称 | 返回类型 | 描述 |
---|---|---|
createEvent(name: string, params?: CreateEventParams) |
可调度事件 |
允许创建新的 NTD 事件,这对于调用任务的执行很有用。事件的名称是必需的。对于第二个可选参数,请参阅下面的 CreateEventParams 对象 API。 |
emit(dispatchableEvent: DispatchableEvent) |
void |
发出一个新创建的事件。通常,除非您正在测试完整的后台工作流程,否则您通常不会使用此功能。 |
listenToEventTrigger(eventName: string, eventId: string) |
Promise<EventData> |
监听并等待具有特定 id(eventId,从 DispatchableEvent.id 获取)的事件(eventName)类型被发出。返回一个包含接收事件有效负载的 promise。 |
这是 CreateEventParams 对象的形状
属性 | 类型 | 描述 |
---|---|---|
data |
EventData |
包含事件有效负载的键值对象。如果不存在,则在创建事件时默认为 {} 。 |
id |
字符串 |
一个字符串,唯一标识此事件实例(对于每次任务调用必须唯一)。在创建任务时默认为新的 UUID。 |
expirationTimestamp |
数字 |
指示处理此事件的任务何时应该完成执行。默认为无。如果提供,则必须是 UNIX 时间戳。 |
有关如何在测试中使用这些 API 的更多信息,您可以查看我们如何实现自己的测试。一些示例包括 SinglePullProvider Spec、BatchPullProvider Spec、StartPushProvider Spec 和 StopPushProvider Spec。
唯一标识符
有时您可能需要创建自己的全球唯一标识符(UUID)。鉴于我们在框架中广泛使用它们,我们认为公开一个生成它们的函数可能很有用,无论是用于创建新的插件还是基于框架的功能。
在 @awarns/core/utils/uuid
文件夹中,您将找到一个具有以下签名的函数
名称 | 返回类型 | 描述 |
---|---|---|
uuid |
字符串 |
使用底层操作系统内置的机制生成新的 UUID。在 Android 上,它使用 UUID.randomUUID() 方法生成 UUID v4。没有涉及其他外部依赖项。 |
数据序列化
在某些情况下,您可能需要将复杂对象结构转换为字符串,并在其他地方恢复原始对象。在其他情况下,您可能只想转换包含混合属性的对象,这些属性包含普通对象和类实例。
在这种情况下,您可以使用我们在框架中广泛使用的内置序列化函数,这些函数位于 @awarns/core/utils/serialization
文件夹中
名称 | 返回类型 | 描述 |
---|---|---|
serialize(data: any) |
字符串 |
将任何复杂的 JavaScript 对象、类实例或数组转换为字符串,该字符串可以恢复。与包含 Date 属性的对象一起使用。 |
deserialize(serializeData: string) |
any |
调用 serialize 方法的反向过程。请注意,类实例将被恢复为普通 JavaScript 对象,失去其原始性质。 |
flatten(data: any) |
any |
与链式调用序列化和反序列化方法相同。利用反序列化副作用将类实例转换为普通对象,以规范化复杂对象结构。 |
许可证
Apache许可证版本2.0